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