1use super::object_change::{AccumulatorWriteV1, ObjectIn, ObjectOut};
5use super::{EffectsObjectChange, IDOperation, ObjectChange};
6use crate::accumulator_event::AccumulatorEvent;
7use crate::accumulator_root::AccumulatorObjId;
8use crate::base_types::{
9 EpochId, ObjectDigest, ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest,
10 VersionDigest,
11};
12use crate::digests::{EffectsAuxDataDigest, TransactionEventsDigest};
13use crate::effects::{InputConsensusObject, TransactionEffectsAPI};
14use crate::execution::SharedInput;
15use crate::execution_status::{
16 ExecutionErrorKind, ExecutionFailure, ExecutionStatus, MoveLocation,
17};
18use crate::gas::GasCostSummary;
19#[cfg(debug_assertions)]
20use crate::is_system_package;
21use crate::object::{OBJECT_START_VERSION, Owner};
22use crate::transaction::SharedObjectMutability;
23use serde::{Deserialize, Serialize};
24#[cfg(debug_assertions)]
25use std::collections::HashSet;
26use std::collections::{BTreeMap, BTreeSet};
27
28#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
30pub struct TransactionEffectsV2 {
31 pub(crate) status: ExecutionStatus,
33 pub(crate) executed_epoch: EpochId,
35 pub(crate) gas_used: GasCostSummary,
36 pub(crate) transaction_digest: TransactionDigest,
38 pub(crate) gas_object_index: Option<u32>,
42 pub(crate) events_digest: Option<TransactionEventsDigest>,
45 pub(crate) dependencies: Vec<TransactionDigest>,
47
48 pub(crate) lamport_version: SequenceNumber,
50 pub(crate) changed_objects: Vec<(ObjectID, EffectsObjectChange)>,
58 pub(crate) unchanged_consensus_objects: Vec<(ObjectID, UnchangedConsensusKind)>,
63 pub(crate) aux_data_digest: Option<EffectsAuxDataDigest>,
67}
68
69impl TransactionEffectsAPI for TransactionEffectsV2 {
70 fn status(&self) -> &ExecutionStatus {
71 &self.status
72 }
73
74 fn into_status(self) -> ExecutionStatus {
75 self.status
76 }
77
78 fn executed_epoch(&self) -> EpochId {
79 self.executed_epoch
80 }
81
82 fn modified_at_versions(&self) -> Vec<(ObjectID, SequenceNumber)> {
83 self.changed_objects
84 .iter()
85 .filter_map(|(id, change)| {
86 if let ObjectIn::Exist(((version, _digest), _owner)) = &change.input_state {
87 Some((*id, *version))
88 } else {
89 None
90 }
91 })
92 .collect()
93 }
94
95 fn move_abort(&self) -> Option<(MoveLocation, u64)> {
96 let ExecutionStatus::Failure(ExecutionFailure {
97 error: ExecutionErrorKind::MoveAbort(move_location, code),
98 ..
99 }) = self.status()
100 else {
101 return None;
102 };
103 Some((move_location.clone(), *code))
104 }
105
106 fn lamport_version(&self) -> SequenceNumber {
107 self.lamport_version
108 }
109
110 fn old_object_metadata(&self) -> Vec<(ObjectRef, Owner)> {
111 self.changed_objects
112 .iter()
113 .filter_map(|(id, change)| {
114 if let ObjectIn::Exist(((version, digest), owner)) = &change.input_state {
115 Some(((*id, *version, *digest), owner.clone()))
116 } else {
117 None
118 }
119 })
120 .collect()
121 }
122
123 fn input_consensus_objects(&self) -> Vec<InputConsensusObject> {
124 self.changed_objects
125 .iter()
126 .filter_map(|(id, change)| match &change.input_state {
127 ObjectIn::Exist(((version, digest), owner)) if owner.is_consensus() => {
128 Some(InputConsensusObject::Mutate((*id, *version, *digest)))
129 }
130 _ => None,
131 })
132 .chain(
133 self.unchanged_consensus_objects
134 .iter()
135 .filter_map(|(id, change_kind)| match change_kind {
136 UnchangedConsensusKind::ReadOnlyRoot((version, digest)) => {
137 Some(InputConsensusObject::ReadOnly((*id, *version, *digest)))
138 }
139 UnchangedConsensusKind::MutateConsensusStreamEnded(seqno) => Some(
140 InputConsensusObject::MutateConsensusStreamEnded(*id, *seqno),
141 ),
142 UnchangedConsensusKind::ReadConsensusStreamEnded(seqno) => {
143 Some(InputConsensusObject::ReadConsensusStreamEnded(*id, *seqno))
144 }
145 UnchangedConsensusKind::Cancelled(seqno) => {
146 Some(InputConsensusObject::Cancelled(*id, *seqno))
147 }
148 UnchangedConsensusKind::PerEpochConfig => None,
152 }),
153 )
154 .collect()
155 }
156
157 fn created(&self) -> Vec<(ObjectRef, Owner)> {
158 self.changed_objects
159 .iter()
160 .filter_map(|(id, change)| {
161 match (
162 &change.input_state,
163 &change.output_state,
164 &change.id_operation,
165 ) {
166 (
167 ObjectIn::NotExist,
168 ObjectOut::ObjectWrite((digest, owner)),
169 IDOperation::Created,
170 ) => Some(((*id, self.lamport_version, *digest), owner.clone())),
171 (
172 ObjectIn::NotExist,
173 ObjectOut::PackageWrite((version, digest)),
174 IDOperation::Created,
175 ) => Some(((*id, *version, *digest), Owner::Immutable)),
176 _ => None,
177 }
178 })
179 .collect()
180 }
181
182 fn mutated(&self) -> Vec<(ObjectRef, Owner)> {
183 self.changed_objects
184 .iter()
185 .filter_map(
186 |(id, change)| match (&change.input_state, &change.output_state) {
187 (ObjectIn::Exist(_), ObjectOut::ObjectWrite((digest, owner))) => {
188 Some(((*id, self.lamport_version, *digest), owner.clone()))
189 }
190 (ObjectIn::Exist(_), ObjectOut::PackageWrite((version, digest))) => {
191 Some(((*id, *version, *digest), Owner::Immutable))
192 }
193 _ => None,
194 },
195 )
196 .collect()
197 }
198
199 fn unwrapped(&self) -> Vec<(ObjectRef, Owner)> {
200 self.changed_objects
201 .iter()
202 .filter_map(|(id, change)| {
203 match (
204 &change.input_state,
205 &change.output_state,
206 &change.id_operation,
207 ) {
208 (
209 ObjectIn::NotExist,
210 ObjectOut::ObjectWrite((digest, owner)),
211 IDOperation::None,
212 ) => Some(((*id, self.lamport_version, *digest), owner.clone())),
213 _ => None,
214 }
215 })
216 .collect()
217 }
218
219 fn deleted(&self) -> Vec<ObjectRef> {
220 self.changed_objects
221 .iter()
222 .filter_map(|(id, change)| {
223 match (
224 &change.input_state,
225 &change.output_state,
226 &change.id_operation,
227 ) {
228 (ObjectIn::Exist(_), ObjectOut::NotExist, IDOperation::Deleted) => Some((
229 *id,
230 self.lamport_version,
231 ObjectDigest::OBJECT_DIGEST_DELETED,
232 )),
233 _ => None,
234 }
235 })
236 .collect()
237 }
238
239 fn unwrapped_then_deleted(&self) -> Vec<ObjectRef> {
240 self.changed_objects
241 .iter()
242 .filter_map(|(id, change)| {
243 match (
244 &change.input_state,
245 &change.output_state,
246 &change.id_operation,
247 ) {
248 (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Deleted) => Some((
249 *id,
250 self.lamport_version,
251 ObjectDigest::OBJECT_DIGEST_DELETED,
252 )),
253 _ => None,
254 }
255 })
256 .collect()
257 }
258
259 fn wrapped(&self) -> Vec<ObjectRef> {
260 self.changed_objects
261 .iter()
262 .filter_map(|(id, change)| {
263 match (
264 &change.input_state,
265 &change.output_state,
266 &change.id_operation,
267 ) {
268 (ObjectIn::Exist(_), ObjectOut::NotExist, IDOperation::None) => Some((
269 *id,
270 self.lamport_version,
271 ObjectDigest::OBJECT_DIGEST_WRAPPED,
272 )),
273 _ => None,
274 }
275 })
276 .collect()
277 }
278
279 fn written(&self) -> Vec<ObjectRef> {
280 self.changed_objects
281 .iter()
282 .filter_map(
283 |(id, change)| match (&change.output_state, &change.id_operation) {
284 (ObjectOut::NotExist, IDOperation::Deleted) => Some((
285 *id,
286 self.lamport_version,
287 ObjectDigest::OBJECT_DIGEST_DELETED,
288 )),
289 (ObjectOut::NotExist, IDOperation::None) => Some((
290 *id,
291 self.lamport_version,
292 ObjectDigest::OBJECT_DIGEST_WRAPPED,
293 )),
294 (ObjectOut::ObjectWrite((d, _)), _) => Some((*id, self.lamport_version, *d)),
295 (ObjectOut::PackageWrite(vd), _) => Some((*id, vd.0, vd.1)),
296 (ObjectOut::AccumulatorWriteV1(_), _) => None,
297 _ => None,
298 },
299 )
300 .collect()
301 }
302
303 fn transferred_from_consensus(&self) -> Vec<ObjectRef> {
304 self.changed_objects
305 .iter()
306 .filter_map(|(id, change)| {
307 match (
308 &change.input_state,
309 &change.output_state,
310 &change.id_operation,
311 ) {
312 (
313 ObjectIn::Exist((_, Owner::ConsensusAddressOwner { .. })),
314 ObjectOut::ObjectWrite((
315 object_digest,
316 Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable,
317 )),
318 IDOperation::None,
319 ) => Some((*id, self.lamport_version, *object_digest)),
320 _ => None,
321 }
322 })
323 .collect()
324 }
325
326 fn transferred_to_consensus(&self) -> Vec<ObjectRef> {
327 self.changed_objects
328 .iter()
329 .filter_map(|(id, change)| {
330 match (
331 &change.input_state,
332 &change.output_state,
333 &change.id_operation,
334 ) {
335 (
336 ObjectIn::Exist((_, Owner::AddressOwner(_) | Owner::ObjectOwner(_))),
337 ObjectOut::ObjectWrite((
338 object_digest,
339 Owner::ConsensusAddressOwner { .. },
340 )),
341 IDOperation::None,
342 ) => Some((*id, self.lamport_version, *object_digest)),
343 _ => None,
344 }
345 })
346 .collect()
347 }
348
349 fn consensus_owner_changed(&self) -> Vec<ObjectRef> {
350 self.changed_objects
351 .iter()
352 .filter_map(|(id, change)| {
353 match (
354 &change.input_state,
355 &change.output_state,
356 &change.id_operation,
357 ) {
358 (
359 ObjectIn::Exist((
360 _,
361 Owner::ConsensusAddressOwner {
362 owner: old_owner, ..
363 },
364 )),
365 ObjectOut::ObjectWrite((
366 object_digest,
367 Owner::ConsensusAddressOwner {
368 owner: new_owner, ..
369 },
370 )),
371 IDOperation::None,
372 ) if old_owner != new_owner => {
373 Some((*id, self.lamport_version, *object_digest))
374 }
375 _ => None,
376 }
377 })
378 .collect()
379 }
380
381 fn object_changes(&self) -> Vec<ObjectChange> {
382 self.changed_objects
383 .iter()
384 .filter_map(|(id, change)| {
385 let input_version_digest = match &change.input_state {
386 ObjectIn::NotExist => None,
387 ObjectIn::Exist((vd, _)) => Some(*vd),
388 };
389
390 let output_version_digest = match &change.output_state {
391 ObjectOut::NotExist => None,
392 ObjectOut::ObjectWrite((d, _)) => Some((self.lamport_version, *d)),
393 ObjectOut::PackageWrite(vd) => Some(*vd),
394 ObjectOut::AccumulatorWriteV1(_) => {
395 return None;
396 }
397 };
398
399 Some(ObjectChange {
400 id: *id,
401
402 input_version: input_version_digest.map(|k| k.0),
403 input_digest: input_version_digest.map(|k| k.1),
404
405 output_version: output_version_digest.map(|k| k.0),
406 output_digest: output_version_digest.map(|k| k.1),
407
408 id_operation: change.id_operation,
409 })
410 })
411 .collect()
412 }
413
414 fn published_packages(&self) -> Vec<ObjectID> {
415 self.changed_objects
416 .iter()
417 .filter_map(|(id, change)| {
418 if matches!(&change.output_state, ObjectOut::PackageWrite(_)) {
419 Some(*id)
420 } else {
421 None
422 }
423 })
424 .collect()
425 }
426
427 fn accumulator_events(&self) -> Vec<AccumulatorEvent> {
428 self.changed_objects
429 .iter()
430 .filter_map(|(id, change)| match &change.output_state {
431 ObjectOut::AccumulatorWriteV1(write) => Some(AccumulatorEvent::new(
432 AccumulatorObjId::new_unchecked(*id),
433 write.clone(),
434 )),
435 _ => None,
436 })
437 .collect()
438 }
439
440 fn gas_object(&self) -> Option<(ObjectRef, Owner)> {
441 self.gas_object_index.map(|index| {
442 let entry = &self.changed_objects[index as usize];
443 match &entry.1.output_state {
444 ObjectOut::ObjectWrite((digest, owner)) => {
445 ((entry.0, self.lamport_version, *digest), owner.clone())
446 }
447 ObjectOut::NotExist => {
448 (
451 (
452 entry.0,
453 self.lamport_version,
454 ObjectDigest::OBJECT_DIGEST_DELETED,
455 ),
456 Owner::AddressOwner(SuiAddress::default()),
457 )
458 }
459 _ => panic!("Gas object must be an ObjectWrite or Deleted in changed_objects"),
460 }
461 })
462 }
463
464 fn events_digest(&self) -> Option<&TransactionEventsDigest> {
465 self.events_digest.as_ref()
466 }
467
468 fn dependencies(&self) -> &[TransactionDigest] {
469 &self.dependencies
470 }
471
472 fn transaction_digest(&self) -> &TransactionDigest {
473 &self.transaction_digest
474 }
475
476 fn gas_cost_summary(&self) -> &GasCostSummary {
477 &self.gas_used
478 }
479
480 fn unchanged_consensus_objects(&self) -> Vec<(ObjectID, UnchangedConsensusKind)> {
481 self.unchanged_consensus_objects.clone()
482 }
483
484 fn accumulator_updates(&self) -> Vec<(ObjectID, AccumulatorWriteV1)> {
485 self.changed_objects
486 .iter()
487 .filter_map(|(id, change)| match &change.output_state {
488 ObjectOut::AccumulatorWriteV1(update) => Some((*id, update.clone())),
489 _ => None,
490 })
491 .collect()
492 }
493
494 fn status_mut_for_testing(&mut self) -> &mut ExecutionStatus {
495 &mut self.status
496 }
497
498 fn gas_cost_summary_mut_for_testing(&mut self) -> &mut GasCostSummary {
499 &mut self.gas_used
500 }
501
502 fn transaction_digest_mut_for_testing(&mut self) -> &mut TransactionDigest {
503 &mut self.transaction_digest
504 }
505
506 fn dependencies_mut_for_testing(&mut self) -> &mut Vec<TransactionDigest> {
507 &mut self.dependencies
508 }
509
510 fn unsafe_add_input_consensus_object_for_testing(&mut self, kind: InputConsensusObject) {
511 match kind {
512 InputConsensusObject::Mutate(obj_ref) => self.changed_objects.push((
513 obj_ref.0,
514 EffectsObjectChange {
515 input_state: ObjectIn::Exist((
516 (obj_ref.1, obj_ref.2),
517 Owner::Shared {
518 initial_shared_version: OBJECT_START_VERSION,
519 },
520 )),
521 output_state: ObjectOut::ObjectWrite((
522 obj_ref.2,
523 Owner::Shared {
524 initial_shared_version: obj_ref.1,
525 },
526 )),
527 id_operation: IDOperation::None,
528 },
529 )),
530 InputConsensusObject::ReadOnly(obj_ref) => self.unchanged_consensus_objects.push((
531 obj_ref.0,
532 UnchangedConsensusKind::ReadOnlyRoot((obj_ref.1, obj_ref.2)),
533 )),
534 InputConsensusObject::ReadConsensusStreamEnded(obj_id, seqno) => {
535 self.unchanged_consensus_objects.push((
536 obj_id,
537 UnchangedConsensusKind::ReadConsensusStreamEnded(seqno),
538 ))
539 }
540 InputConsensusObject::MutateConsensusStreamEnded(obj_id, seqno) => {
541 self.unchanged_consensus_objects.push((
542 obj_id,
543 UnchangedConsensusKind::MutateConsensusStreamEnded(seqno),
544 ))
545 }
546 InputConsensusObject::Cancelled(obj_id, seqno) => self
547 .unchanged_consensus_objects
548 .push((obj_id, UnchangedConsensusKind::Cancelled(seqno))),
549 }
550 }
551
552 fn unsafe_add_deleted_live_object_for_testing(&mut self, obj_ref: ObjectRef) {
553 self.changed_objects.push((
554 obj_ref.0,
555 EffectsObjectChange {
556 input_state: ObjectIn::Exist((
557 (obj_ref.1, obj_ref.2),
558 Owner::AddressOwner(SuiAddress::default()),
559 )),
560 output_state: ObjectOut::ObjectWrite((
561 obj_ref.2,
562 Owner::AddressOwner(SuiAddress::default()),
563 )),
564 id_operation: IDOperation::None,
565 },
566 ))
567 }
568
569 fn unsafe_add_object_tombstone_for_testing(&mut self, obj_ref: ObjectRef) {
570 self.changed_objects.push((
571 obj_ref.0,
572 EffectsObjectChange {
573 input_state: ObjectIn::Exist((
574 (obj_ref.1, obj_ref.2),
575 Owner::AddressOwner(SuiAddress::default()),
576 )),
577 output_state: ObjectOut::NotExist,
578 id_operation: IDOperation::Deleted,
579 },
580 ))
581 }
582}
583
584impl TransactionEffectsV2 {
585 pub fn new(
586 status: ExecutionStatus,
587 executed_epoch: EpochId,
588 gas_used: GasCostSummary,
589 shared_objects: Vec<SharedInput>,
590 loaded_per_epoch_config_objects: BTreeSet<ObjectID>,
591 transaction_digest: TransactionDigest,
592 lamport_version: SequenceNumber,
593 changed_objects: BTreeMap<ObjectID, EffectsObjectChange>,
594 gas_object: Option<ObjectID>,
595 events_digest: Option<TransactionEventsDigest>,
596 dependencies: Vec<TransactionDigest>,
597 ) -> Self {
598 let unchanged_consensus_objects = shared_objects
599 .into_iter()
600 .filter_map(|shared_input| match shared_input {
601 SharedInput::Existing((id, version, digest)) => {
602 if changed_objects.contains_key(&id) {
603 None
604 } else {
605 Some((id, UnchangedConsensusKind::ReadOnlyRoot((version, digest))))
606 }
607 }
608 SharedInput::ConsensusStreamEnded((id, version, mutability, _)) => {
609 debug_assert!(!changed_objects.contains_key(&id));
610 match mutability {
611 SharedObjectMutability::Mutable => Some((
612 id,
613 UnchangedConsensusKind::MutateConsensusStreamEnded(version),
614 )),
615 SharedObjectMutability::Immutable => Some((
616 id,
617 UnchangedConsensusKind::ReadConsensusStreamEnded(version),
618 )),
619 SharedObjectMutability::NonExclusiveWrite => Some((
622 id,
623 UnchangedConsensusKind::MutateConsensusStreamEnded(version),
624 )),
625 }
626 }
627 SharedInput::Cancelled((id, version)) => {
628 debug_assert!(!changed_objects.contains_key(&id));
629 Some((id, UnchangedConsensusKind::Cancelled(version)))
630 }
631 })
632 .chain(
633 loaded_per_epoch_config_objects
634 .into_iter()
635 .map(|id| (id, UnchangedConsensusKind::PerEpochConfig)),
636 )
637 .collect();
638 let changed_objects: Vec<_> = changed_objects.into_iter().collect();
639
640 let gas_object_index = gas_object.map(|gas_id| {
641 changed_objects
642 .iter()
643 .position(|(id, _)| id == &gas_id)
644 .unwrap() as u32
645 });
646
647 let result = Self {
648 status,
649 executed_epoch,
650 gas_used,
651 transaction_digest,
652 lamport_version,
653 changed_objects,
654 unchanged_consensus_objects,
655 gas_object_index,
656 events_digest,
657 dependencies,
658 aux_data_digest: None,
659 };
660 #[cfg(debug_assertions)]
661 result.check_invariant();
662
663 result
664 }
665
666 #[cfg(debug_assertions)]
669 fn check_invariant(&self) {
670 let mut unique_ids = HashSet::new();
671 for (id, change) in &self.changed_objects {
672 assert!(unique_ids.insert(*id));
673 match (
674 &change.input_state,
675 &change.output_state,
676 &change.id_operation,
677 ) {
678 (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Created) => {
679 }
681 (ObjectIn::NotExist, ObjectOut::NotExist, IDOperation::Deleted) => {
682 }
684 (ObjectIn::NotExist, ObjectOut::ObjectWrite((_, owner)), IDOperation::None) => {
685 assert!(!owner.is_shared());
688 }
689 (ObjectIn::NotExist, ObjectOut::ObjectWrite(..), IDOperation::Created) => {
690 }
692 (ObjectIn::NotExist, ObjectOut::PackageWrite(_), IDOperation::Created) => {
693 }
695 (
696 ObjectIn::Exist(((old_version, _), old_owner)),
697 ObjectOut::NotExist,
698 IDOperation::None,
699 ) => {
700 assert!(old_version.value() < self.lamport_version.value());
702 assert!(
703 !old_owner.is_shared() && !old_owner.is_immutable(),
704 "Cannot wrap shared or immutable object"
705 );
706 }
707 (
708 ObjectIn::Exist(((old_version, _), old_owner)),
709 ObjectOut::NotExist,
710 IDOperation::Deleted,
711 ) => {
712 assert!(old_version.value() < self.lamport_version.value());
714 assert!(!old_owner.is_immutable(), "Cannot delete immutable object");
715 }
716 (
717 ObjectIn::Exist(((old_version, old_digest), old_owner)),
718 ObjectOut::ObjectWrite((new_digest, new_owner)),
719 IDOperation::None,
720 ) => {
721 assert!(old_version.value() < self.lamport_version.value());
723 assert_ne!(old_digest, new_digest);
724 assert!(!old_owner.is_immutable(), "Cannot mutate immutable object");
725 if old_owner.is_shared() {
726 assert!(new_owner.is_shared(), "Cannot un-share an object");
727 } else {
728 assert!(!new_owner.is_shared(), "Cannot share an existing object");
729 }
730 }
731 (
732 ObjectIn::Exist(((old_version, old_digest), old_owner)),
733 ObjectOut::PackageWrite((new_version, new_digest)),
734 IDOperation::None,
735 ) => {
736 assert!(
738 old_owner.is_immutable() && is_system_package(*id),
739 "Must be a system package"
740 );
741 assert_eq!(old_version.value() + 1, new_version.value());
742 assert_ne!(old_digest, new_digest);
743 }
744 (ObjectIn::NotExist, ObjectOut::AccumulatorWriteV1(_), IDOperation::None) => {
745 }
747 _ => {
748 panic!("Impossible object change: {:?}, {:?}", id, change);
749 }
750 }
751 }
752 if let Some((_, owner)) = self.gas_object() {
754 assert!(matches!(owner, Owner::AddressOwner(_)));
755 }
756
757 for (id, _) in &self.unchanged_consensus_objects {
758 assert!(
759 unique_ids.insert(*id),
760 "Duplicate object id: {:?}\n{:#?}",
761 id,
762 self
763 );
764 }
765 }
766}
767
768impl Default for TransactionEffectsV2 {
769 fn default() -> Self {
770 Self {
771 status: ExecutionStatus::Success,
772 executed_epoch: 0,
773 gas_used: GasCostSummary::default(),
774 transaction_digest: TransactionDigest::default(),
775 lamport_version: SequenceNumber::default(),
776 changed_objects: vec![],
777 unchanged_consensus_objects: vec![],
778 gas_object_index: None,
779 events_digest: None,
780 dependencies: vec![],
781 aux_data_digest: None,
782 }
783 }
784}
785
786#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
787pub enum UnchangedConsensusKind {
788 ReadOnlyRoot(VersionDigest),
791 MutateConsensusStreamEnded(SequenceNumber),
793 ReadConsensusStreamEnded(SequenceNumber),
795 Cancelled(SequenceNumber),
797 PerEpochConfig,
799}