iota_core/authority/
shared_object_version_manager.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6
7use iota_types::{
8    IOTA_RANDOMNESS_STATE_OBJECT_ID,
9    base_types::{ObjectID, SequenceNumber, TransactionDigest},
10    crypto::RandomnessRound,
11    effects::{TransactionEffects, TransactionEffectsAPI},
12    error::IotaResult,
13    executable_transaction::VerifiedExecutableTransaction,
14    storage::{
15        ObjectKey, transaction_non_shared_input_object_keys, transaction_receiving_object_keys,
16    },
17    transaction::{SenderSignedData, SharedInputObject, TransactionDataAPI, TransactionKey},
18};
19use tracing::{debug, trace};
20
21use crate::{
22    authority::{
23        AuthorityPerEpochStore, authority_per_epoch_store::CancelConsensusCertificateReason,
24        epoch_start_configuration::EpochStartConfigTrait,
25    },
26    execution_cache::ObjectCacheRead,
27};
28
29pub struct SharedObjVerManager {}
30
31pub type AssignedTxAndVersions = Vec<(TransactionKey, Vec<(ObjectID, SequenceNumber)>)>;
32
33#[must_use]
34#[derive(Default)]
35pub struct ConsensusSharedObjVerAssignment {
36    pub shared_input_next_versions: HashMap<ObjectID, SequenceNumber>,
37    pub assigned_versions: AssignedTxAndVersions,
38}
39
40impl SharedObjVerManager {
41    pub fn assign_versions_from_consensus(
42        epoch_store: &AuthorityPerEpochStore,
43        cache_reader: &dyn ObjectCacheRead,
44        certificates: &[VerifiedExecutableTransaction],
45        randomness_round: Option<RandomnessRound>,
46        cancelled_txns: &BTreeMap<TransactionDigest, CancelConsensusCertificateReason>,
47    ) -> IotaResult<ConsensusSharedObjVerAssignment> {
48        let mut shared_input_next_versions = get_or_init_versions(
49            certificates.iter().map(|cert| cert.data()),
50            epoch_store,
51            cache_reader,
52            randomness_round.is_some(),
53        )?;
54        let mut assigned_versions = Vec::new();
55        // We must update randomness object version first before processing any
56        // transaction, so that all reads are using the next version.
57        // TODO: Add a test that actually check this, i.e. if we change the order, some
58        // test should fail.
59        if let Some(round) = randomness_round {
60            // If we're generating randomness, update the randomness state object version.
61            let version = shared_input_next_versions
62                .get_mut(&IOTA_RANDOMNESS_STATE_OBJECT_ID)
63                .expect("randomness state object must have been added in get_or_init_versions()");
64            debug!(
65                "assigning shared object versions for randomness: epoch {}, round {round:?} -> version {version:?}",
66                epoch_store.epoch()
67            );
68            assigned_versions.push((
69                TransactionKey::RandomnessRound(epoch_store.epoch(), round),
70                vec![(IOTA_RANDOMNESS_STATE_OBJECT_ID, *version)],
71            ));
72            version.increment();
73        }
74        for cert in certificates {
75            if !cert.contains_shared_object() {
76                continue;
77            }
78            let cert_assigned_versions = Self::assign_versions_for_certificate(
79                cert,
80                &mut shared_input_next_versions,
81                cancelled_txns,
82                epoch_store
83                    .protocol_config()
84                    .congestion_control_gas_price_feedback_mechanism(),
85            );
86            assigned_versions.push((cert.key(), cert_assigned_versions));
87        }
88
89        Ok(ConsensusSharedObjVerAssignment {
90            shared_input_next_versions,
91            assigned_versions,
92        })
93    }
94
95    pub fn assign_versions_from_effects(
96        certs_and_effects: &[(&VerifiedExecutableTransaction, &TransactionEffects)],
97        epoch_store: &AuthorityPerEpochStore,
98        cache_reader: &dyn ObjectCacheRead,
99    ) -> AssignedTxAndVersions {
100        // We don't care about the results since we can use effects to assign versions.
101        // But we must call it to make sure whenever a shared object is touched the
102        // first time during an epoch, either through consensus or through
103        // checkpoint executor, its next version must be initialized. This is
104        // because we initialize the next version of a shared object in an epoch
105        // by reading the current version from the object store. This must be
106        // done before we mutate it the first time, otherwise we would be initializing
107        // it with the wrong version.
108        let _ = get_or_init_versions(
109            certs_and_effects.iter().map(|(cert, _)| cert.data()),
110            epoch_store,
111            cache_reader,
112            false,
113        );
114        let mut assigned_versions = Vec::new();
115        for (cert, effects) in certs_and_effects {
116            let cert_assigned_versions: Vec<_> = effects
117                .input_shared_objects()
118                .into_iter()
119                .map(|iso| iso.id_and_version())
120                .collect();
121            let tx_key = cert.key();
122            trace!(
123                ?tx_key,
124                ?cert_assigned_versions,
125                "locking shared objects from effects"
126            );
127            assigned_versions.push((tx_key, cert_assigned_versions));
128        }
129        assigned_versions
130    }
131
132    pub fn assign_versions_for_certificate(
133        cert: &VerifiedExecutableTransaction,
134        shared_input_next_versions: &mut HashMap<ObjectID, SequenceNumber>,
135        cancelled_txns: &BTreeMap<TransactionDigest, CancelConsensusCertificateReason>,
136        enable_gas_price_feedback: bool,
137    ) -> Vec<(ObjectID, SequenceNumber)> {
138        let tx_digest = cert.digest();
139
140        // Check if the transaction is cancelled due to congestion.
141        let cancellation_info = cancelled_txns.get(tx_digest);
142        let congested_objects_info: Option<HashSet<_>> =
143            if let Some(CancelConsensusCertificateReason::CongestionOnObjects {
144                congested_objects,
145                suggested_gas_price: _,
146            }) = &cancellation_info
147            {
148                Some(congested_objects.iter().cloned().collect())
149            } else {
150                None
151            };
152        let txn_cancelled = cancellation_info.is_some();
153
154        // Make an iterator to update the locks of the transaction's shared objects.
155        let shared_input_objects: Vec<_> = cert.shared_input_objects().collect();
156
157        let mut input_object_keys = transaction_non_shared_input_object_keys(cert)
158            .expect("Transaction input should have been verified");
159        let mut assigned_versions = Vec::with_capacity(shared_input_objects.len());
160        let mut is_mutable_input = Vec::with_capacity(shared_input_objects.len());
161        // Record receiving object versions towards the shared version computation.
162        let receiving_object_keys = transaction_receiving_object_keys(cert);
163        input_object_keys.extend(receiving_object_keys);
164
165        if txn_cancelled {
166            // For cancelled transaction due to congestion, assign special versions to all
167            // shared objects. Note that new lamport version does not depend on
168            // any shared objects.
169            for SharedInputObject { id, .. } in shared_input_objects.iter() {
170                let assigned_version = match cancellation_info {
171                    Some(CancelConsensusCertificateReason::CongestionOnObjects {
172                        congested_objects: _,
173                        suggested_gas_price,
174                    }) => {
175                        if congested_objects_info
176                            .as_ref()
177                            .is_some_and(|info| info.contains(id))
178                        {
179                            if enable_gas_price_feedback {
180                                SequenceNumber::new_congested_with_suggested_gas_price(
181                                    suggested_gas_price.expect(
182                                        "Suggested gas price for transactions cancelled due \
183                                            to congestion must not be None if the gas price \
184                                            feedback is enabled.",
185                                    ),
186                                )
187                            } else {
188                                // WARN: do not remove this `else` branch even after
189                                // `congestion_control_gas_price_feedback_mechanism` is enabled
190                                // on the mainnet. It must be kept to be able to replay old
191                                // transaction data.
192                                SequenceNumber::CONGESTED_PRIOR_TO_GAS_PRICE_FEEDBACK
193                            }
194                        } else {
195                            SequenceNumber::CANCELLED_READ
196                        }
197                    }
198                    Some(CancelConsensusCertificateReason::DkgFailed) => {
199                        if id == &IOTA_RANDOMNESS_STATE_OBJECT_ID {
200                            SequenceNumber::RANDOMNESS_UNAVAILABLE
201                        } else {
202                            SequenceNumber::CANCELLED_READ
203                        }
204                    }
205                    None => unreachable!("cancelled transaction should have cancellation info"),
206                };
207                assigned_versions.push((*id, assigned_version));
208                is_mutable_input.push(false);
209            }
210        } else {
211            for (SharedInputObject { id, mutable, .. }, assigned_version) in shared_input_objects
212                .iter()
213                .map(|obj| (obj, *shared_input_next_versions.get(&obj.id()).unwrap()))
214            {
215                assigned_versions.push((*id, assigned_version));
216                input_object_keys.push(ObjectKey(*id, assigned_version));
217                is_mutable_input.push(*mutable);
218            }
219        }
220
221        let next_version =
222            SequenceNumber::lamport_increment(input_object_keys.iter().map(|obj| obj.1));
223        assert!(
224            next_version.is_valid(),
225            "Assigned version must be valid. Got {next_version:?}"
226        );
227
228        if !txn_cancelled {
229            // Update the next version for the shared objects.
230            assigned_versions
231                .iter()
232                .zip(is_mutable_input)
233                .filter_map(|((id, _), mutable)| {
234                    if mutable {
235                        Some((*id, next_version))
236                    } else {
237                        None
238                    }
239                })
240                .for_each(|(id, version)| {
241                    assert!(
242                        version.is_valid(),
243                        "Assigned version must be a valid version."
244                    );
245                    shared_input_next_versions
246                        .insert(id, version)
247                        .expect("Object must exist in shared_input_next_versions.");
248                });
249        }
250
251        trace!(
252            ?tx_digest,
253            ?assigned_versions,
254            ?next_version,
255            ?txn_cancelled,
256            "locking shared objects"
257        );
258
259        assigned_versions
260    }
261}
262
263fn get_or_init_versions<'a>(
264    transactions: impl Iterator<Item = &'a SenderSignedData>,
265    epoch_store: &AuthorityPerEpochStore,
266    cache_reader: &dyn ObjectCacheRead,
267    generate_randomness: bool,
268) -> IotaResult<HashMap<ObjectID, SequenceNumber>> {
269    let mut shared_input_objects: Vec<_> = transactions
270        .flat_map(|tx| {
271            tx.transaction_data()
272                .shared_input_objects()
273                .into_iter()
274                .map(|so| so.into_id_and_version())
275        })
276        .collect();
277
278    if generate_randomness {
279        shared_input_objects.push((
280            IOTA_RANDOMNESS_STATE_OBJECT_ID,
281            epoch_store
282                .epoch_start_config()
283                .randomness_obj_initial_shared_version(),
284        ));
285    }
286
287    shared_input_objects.sort();
288    shared_input_objects.dedup();
289
290    epoch_store.get_or_init_next_object_versions(&shared_input_objects, cache_reader)
291}
292
293#[cfg(test)]
294mod tests {
295    use std::collections::{BTreeMap, HashMap};
296
297    use iota_test_transaction_builder::TestTransactionBuilder;
298    use iota_types::{
299        IOTA_RANDOMNESS_STATE_OBJECT_ID,
300        base_types::{IotaAddress, ObjectID, SequenceNumber},
301        crypto::RandomnessRound,
302        digests::ObjectDigest,
303        effects::TestEffectsBuilder,
304        executable_transaction::{
305            CertificateProof, ExecutableTransaction, VerifiedExecutableTransaction,
306        },
307        object::{Object, Owner},
308        programmable_transaction_builder::ProgrammableTransactionBuilder,
309        transaction::{ObjectArg, SenderSignedData, TransactionKey},
310    };
311
312    use super::*;
313    use crate::authority::{
314        epoch_start_configuration::EpochStartConfigTrait,
315        shared_object_version_manager::{ConsensusSharedObjVerAssignment, SharedObjVerManager},
316        test_authority_builder::TestAuthorityBuilder,
317    };
318
319    #[tokio::test]
320    async fn test_assign_versions_from_consensus_basic() {
321        let shared_object = Object::shared_for_testing();
322        let id = shared_object.id();
323        let init_shared_version = match shared_object.owner {
324            Owner::Shared {
325                initial_shared_version,
326                ..
327            } => initial_shared_version,
328            _ => panic!("expected shared object"),
329        };
330        let authority = TestAuthorityBuilder::new()
331            .with_starting_objects(std::slice::from_ref(&shared_object))
332            .build()
333            .await;
334        let certs = vec![
335            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 3),
336            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, false)], 5),
337            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 9),
338            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 11),
339        ];
340        let epoch_store = authority.epoch_store_for_testing();
341        let ConsensusSharedObjVerAssignment {
342            shared_input_next_versions,
343            assigned_versions,
344        } = SharedObjVerManager::assign_versions_from_consensus(
345            &epoch_store,
346            authority.get_object_cache_reader().as_ref(),
347            &certs,
348            None,
349            &BTreeMap::new(),
350        )
351        .unwrap();
352        // Check that the shared object's next version is always initialized in the
353        // epoch store.
354        assert_eq!(
355            epoch_store.get_next_object_version(&id).unwrap(),
356            init_shared_version
357        );
358        // Check that the final version of the shared object is the lamport version of
359        // the last transaction.
360        assert_eq!(
361            shared_input_next_versions,
362            HashMap::from([(id, SequenceNumber::from_u64(12))])
363        );
364        // Check that the version assignment for each transaction is correct.
365        // For a transaction that uses the shared object with mutable=false, it won't
366        // update the version using lamport version, hence the next transaction
367        // will use the same version number. In the following case, certs[2] has
368        // the same assignment as certs[1] for this reason.
369        assert_eq!(
370            assigned_versions,
371            vec![
372                (certs[0].key(), vec![(id, init_shared_version),]),
373                (certs[1].key(), vec![(id, SequenceNumber::from_u64(4)),]),
374                (certs[2].key(), vec![(id, SequenceNumber::from_u64(4)),]),
375                (certs[3].key(), vec![(id, SequenceNumber::from_u64(10)),]),
376            ]
377        );
378    }
379
380    #[tokio::test]
381    async fn test_assign_versions_from_consensus_with_randomness() {
382        let authority = TestAuthorityBuilder::new().build().await;
383        let epoch_store = authority.epoch_store_for_testing();
384        let randomness_obj_version = epoch_store
385            .epoch_start_config()
386            .randomness_obj_initial_shared_version();
387        let certs = vec![
388            generate_shared_objs_tx_with_gas_version(
389                &[(
390                    IOTA_RANDOMNESS_STATE_OBJECT_ID,
391                    randomness_obj_version,
392                    // This can only be false since it's not allowed to use randomness object with
393                    // mutable=true.
394                    false,
395                )],
396                3,
397            ),
398            generate_shared_objs_tx_with_gas_version(
399                &[(
400                    IOTA_RANDOMNESS_STATE_OBJECT_ID,
401                    randomness_obj_version,
402                    false,
403                )],
404                5,
405            ),
406        ];
407        let ConsensusSharedObjVerAssignment {
408            shared_input_next_versions,
409            assigned_versions,
410        } = SharedObjVerManager::assign_versions_from_consensus(
411            &epoch_store,
412            authority.get_object_cache_reader().as_ref(),
413            &certs,
414            Some(RandomnessRound::new(1)),
415            &BTreeMap::new(),
416        )
417        .unwrap();
418        // Check that the randomness object's next version is initialized.
419        assert_eq!(
420            epoch_store
421                .get_next_object_version(&IOTA_RANDOMNESS_STATE_OBJECT_ID)
422                .unwrap(),
423            randomness_obj_version
424        );
425        let next_randomness_obj_version = randomness_obj_version.next();
426        assert_eq!(
427            shared_input_next_versions,
428            // Randomness object's version is only incremented by 1 regardless of lamport version.
429            HashMap::from([(IOTA_RANDOMNESS_STATE_OBJECT_ID, next_randomness_obj_version)])
430        );
431        assert_eq!(
432            assigned_versions,
433            vec![
434                (
435                    TransactionKey::RandomnessRound(0, RandomnessRound::new(1)),
436                    vec![(IOTA_RANDOMNESS_STATE_OBJECT_ID, randomness_obj_version),]
437                ),
438                (
439                    certs[0].key(),
440                    // It is critical that the randomness object version is updated before the
441                    // assignment.
442                    vec![(IOTA_RANDOMNESS_STATE_OBJECT_ID, next_randomness_obj_version)]
443                ),
444                (
445                    certs[1].key(),
446                    // It is critical that the randomness object version is updated before the
447                    // assignment.
448                    vec![(IOTA_RANDOMNESS_STATE_OBJECT_ID, next_randomness_obj_version)]
449                ),
450            ]
451        );
452    }
453
454    // Tests shared object version assignment for cancelled transaction.
455    #[tokio::test]
456    async fn test_assign_versions_from_consensus_with_cancellation() {
457        let shared_object_1 = Object::shared_for_testing();
458        let shared_object_2 = Object::shared_for_testing();
459        let id1 = shared_object_1.id();
460        let id2 = shared_object_2.id();
461        let init_shared_version_1 = match shared_object_1.owner {
462            Owner::Shared {
463                initial_shared_version,
464                ..
465            } => initial_shared_version,
466            _ => panic!("expected shared object"),
467        };
468        let init_shared_version_2 = match shared_object_2.owner {
469            Owner::Shared {
470                initial_shared_version,
471                ..
472            } => initial_shared_version,
473            _ => panic!("expected shared object"),
474        };
475        let authority = TestAuthorityBuilder::new()
476            .with_starting_objects(&[shared_object_1.clone(), shared_object_2.clone()])
477            .build()
478            .await;
479        let randomness_obj_version = authority
480            .epoch_store_for_testing()
481            .epoch_start_config()
482            .randomness_obj_initial_shared_version();
483
484        // Generate 5 transactions for testing.
485        //   tx1: shared_object_1, shared_object_2, owned_object_version = 3
486        //   tx2: shared_object_1, shared_object_2, owned_object_version = 5
487        //   tx3: shared_object_1, owned_object_version = 1
488        //   tx4: shared_object_1, shared_object_2, owned_object_version = 9
489        //   tx5: shared_object_1, shared_object_2, owned_object_version = 11
490        //
491        // Later, we cancel transaction 2 and 4 due to congestion, and 5 due to DKG
492        // failure. Expected outcome:
493        //   tx1: both shared objects assign version 1, lamport version = 4
494        //   tx2: shared objects assign cancelled version, lamport version = 6 due to
495        // gas object version = 5   tx3: shared object 1 assign version 4,
496        // lamport version = 5   tx4: shared objects assign cancelled version,
497        // lamport version = 10 due to gas object version = 9   tx5: shared
498        // objects assign cancelled version, lamport version = 12 due to gas object
499        // version = 11
500        let certs = vec![
501            generate_shared_objs_tx_with_gas_version(
502                &[
503                    (id1, init_shared_version_1, true),
504                    (id2, init_shared_version_2, true),
505                ],
506                3,
507            ),
508            generate_shared_objs_tx_with_gas_version(
509                &[
510                    (id1, init_shared_version_1, true),
511                    (id2, init_shared_version_2, true),
512                ],
513                5,
514            ),
515            generate_shared_objs_tx_with_gas_version(&[(id1, init_shared_version_1, true)], 1),
516            generate_shared_objs_tx_with_gas_version(
517                &[
518                    (id1, init_shared_version_1, true),
519                    (id2, init_shared_version_2, true),
520                ],
521                9,
522            ),
523            generate_shared_objs_tx_with_gas_version(
524                &[
525                    (
526                        IOTA_RANDOMNESS_STATE_OBJECT_ID,
527                        randomness_obj_version,
528                        false,
529                    ),
530                    (id2, init_shared_version_2, true),
531                ],
532                11,
533            ),
534        ];
535        let epoch_store = authority.epoch_store_for_testing();
536
537        // Cancel transactions 2 and 4 due to congestion.
538        let suggested_gas_price = 1_000;
539        let cancelled_txns: BTreeMap<TransactionDigest, CancelConsensusCertificateReason> = [
540            (
541                *certs[1].digest(),
542                CancelConsensusCertificateReason::CongestionOnObjects {
543                    congested_objects: vec![id1],
544                    suggested_gas_price: Some(suggested_gas_price),
545                },
546            ),
547            (
548                *certs[3].digest(),
549                CancelConsensusCertificateReason::CongestionOnObjects {
550                    congested_objects: vec![id2],
551                    suggested_gas_price: Some(suggested_gas_price),
552                },
553            ),
554            (
555                *certs[4].digest(),
556                CancelConsensusCertificateReason::DkgFailed,
557            ),
558        ]
559        .into_iter()
560        .collect();
561
562        // Run version assignment logic.
563        let ConsensusSharedObjVerAssignment {
564            shared_input_next_versions,
565            assigned_versions,
566        } = SharedObjVerManager::assign_versions_from_consensus(
567            &epoch_store,
568            authority.get_object_cache_reader().as_ref(),
569            &certs,
570            None,
571            &cancelled_txns,
572        )
573        .unwrap();
574
575        // Check that the final version of the shared object is the lamport version of
576        // the last transaction.
577        assert_eq!(
578            shared_input_next_versions,
579            HashMap::from([
580                (id1, SequenceNumber::from_u64(5)), // determined by tx3
581                (id2, SequenceNumber::from_u64(4)), // determined by tx1
582                (IOTA_RANDOMNESS_STATE_OBJECT_ID, SequenceNumber::from_u64(1)), // not mutable
583            ])
584        );
585
586        // Check that the version assignment for each transaction is correct.
587        assert_eq!(
588            assigned_versions,
589            vec![
590                (
591                    certs[0].key(),
592                    vec![(id1, init_shared_version_1), (id2, init_shared_version_2)]
593                ),
594                (
595                    certs[1].key(),
596                    vec![
597                        (
598                            id1,
599                            SequenceNumber::new_congested_with_suggested_gas_price(
600                                suggested_gas_price
601                            )
602                        ),
603                        (id2, SequenceNumber::CANCELLED_READ),
604                    ]
605                ),
606                (certs[2].key(), vec![(id1, SequenceNumber::from_u64(4)),]),
607                (
608                    certs[3].key(),
609                    vec![
610                        (id1, SequenceNumber::CANCELLED_READ),
611                        (
612                            id2,
613                            SequenceNumber::new_congested_with_suggested_gas_price(
614                                suggested_gas_price
615                            )
616                        )
617                    ]
618                ),
619                (
620                    certs[4].key(),
621                    vec![
622                        (
623                            IOTA_RANDOMNESS_STATE_OBJECT_ID,
624                            SequenceNumber::RANDOMNESS_UNAVAILABLE
625                        ),
626                        (id2, SequenceNumber::CANCELLED_READ)
627                    ]
628                ),
629            ]
630        );
631    }
632
633    #[tokio::test]
634    async fn test_assign_versions_from_effects() {
635        let shared_object = Object::shared_for_testing();
636        let id = shared_object.id();
637        let init_shared_version = match shared_object.owner {
638            Owner::Shared {
639                initial_shared_version,
640                ..
641            } => initial_shared_version,
642            _ => panic!("expected shared object"),
643        };
644        let authority = TestAuthorityBuilder::new()
645            .with_starting_objects(std::slice::from_ref(&shared_object))
646            .build()
647            .await;
648        let certs = [
649            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 3),
650            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, false)], 5),
651            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 9),
652            generate_shared_objs_tx_with_gas_version(&[(id, init_shared_version, true)], 11),
653        ];
654        let effects = [
655            TestEffectsBuilder::new(certs[0].data()).build(),
656            TestEffectsBuilder::new(certs[1].data())
657                .with_shared_input_versions(BTreeMap::from([(id, SequenceNumber::from_u64(4))]))
658                .build(),
659            TestEffectsBuilder::new(certs[2].data())
660                .with_shared_input_versions(BTreeMap::from([(id, SequenceNumber::from_u64(4))]))
661                .build(),
662            TestEffectsBuilder::new(certs[3].data())
663                .with_shared_input_versions(BTreeMap::from([(id, SequenceNumber::from_u64(10))]))
664                .build(),
665        ];
666        let epoch_store = authority.epoch_store_for_testing();
667        let assigned_versions = SharedObjVerManager::assign_versions_from_effects(
668            certs
669                .iter()
670                .zip(effects.iter())
671                .collect::<Vec<_>>()
672                .as_slice(),
673            &epoch_store,
674            authority.get_object_cache_reader().as_ref(),
675        );
676        // Check that the shared object's next version is always initialized in the
677        // epoch store.
678        assert_eq!(
679            epoch_store.get_next_object_version(&id).unwrap(),
680            init_shared_version
681        );
682        assert_eq!(
683            assigned_versions,
684            vec![
685                (certs[0].key(), vec![(id, init_shared_version),]),
686                (certs[1].key(), vec![(id, SequenceNumber::from_u64(4)),]),
687                (certs[2].key(), vec![(id, SequenceNumber::from_u64(4)),]),
688                (certs[3].key(), vec![(id, SequenceNumber::from_u64(10)),]),
689            ]
690        );
691    }
692
693    /// Generate a transaction that uses shared objects as specified in the
694    /// parameters. Also uses a gas object with specified version.
695    /// The version of the gas object is used to manipulate the lamport version
696    /// of this transaction.
697    fn generate_shared_objs_tx_with_gas_version(
698        shared_objects: &[(ObjectID, SequenceNumber, bool)],
699        gas_object_version: u64,
700    ) -> VerifiedExecutableTransaction {
701        let mut builder = ProgrammableTransactionBuilder::new();
702        for (shared_object_id, shared_object_init_version, shared_object_mutable) in shared_objects
703        {
704            builder
705                .obj(ObjectArg::SharedObject {
706                    id: *shared_object_id,
707                    initial_shared_version: *shared_object_init_version,
708                    mutable: *shared_object_mutable,
709                })
710                .unwrap();
711        }
712        let tx_data = TestTransactionBuilder::new(
713            IotaAddress::ZERO,
714            (
715                ObjectID::random(),
716                SequenceNumber::from_u64(gas_object_version),
717                ObjectDigest::random(),
718            ),
719            0,
720        )
721        .programmable(builder.finish())
722        .build();
723        let tx = SenderSignedData::new(tx_data, vec![]);
724        VerifiedExecutableTransaction::new_unchecked(ExecutableTransaction::new_from_data_and_sig(
725            tx,
726            CertificateProof::new_system(0),
727        ))
728    }
729}