Skip to main content

iota_sdk_types/
move_package.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5
6#[cfg(feature = "hash")]
7use crate::hash::Hasher;
8use crate::{
9    Digest, ExecutionError, Identifier, ObjectId,
10    version::{Version, VersionError},
11};
12
13/// Rust representation of upgrade policy constants in `iota::package`.
14#[repr(u8)]
15#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)]
16#[non_exhaustive]
17pub enum UpgradePolicy {
18    /// The least restrictive policy. Permits changes to all function
19    /// implementations, the removal of ability constraints on generic type
20    /// parameters in function signatures, and modifications to private,
21    /// public(friend), and entry function signatures. However, public function
22    /// signatures and existing types cannot be changed.
23    Compatible = 0,
24    /// Allows adding new functionalities (e.g., new public functions or
25    /// structs) but restricts changes to existing functionalities.
26    Additive = 128,
27    /// Limits modifications to the package’s dependencies only.
28    DepOnly = 192,
29}
30
31impl UpgradePolicy {
32    pub const COMPATIBLE: u8 = Self::Compatible as u8;
33    pub const ADDITIVE: u8 = Self::Additive as u8;
34    pub const DEP_ONLY: u8 = Self::DepOnly as u8;
35
36    pub fn is_valid_policy(policy: &u8) -> bool {
37        Self::try_from(*policy).is_ok()
38    }
39}
40
41impl TryFrom<u8> for UpgradePolicy {
42    type Error = ();
43    fn try_from(value: u8) -> Result<Self, Self::Error> {
44        match value {
45            x if x == Self::Compatible as u8 => Ok(Self::Compatible),
46            x if x == Self::Additive as u8 => Ok(Self::Additive),
47            x if x == Self::DepOnly as u8 => Ok(Self::DepOnly),
48            _ => Err(()),
49        }
50    }
51}
52
53/// Type corresponding to the output of `iota move build
54/// --dump-bytecode-as-base64`
55#[derive(Clone, derive_more::Debug)]
56#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
57pub struct MovePackageData {
58    /// The package modules as a series of bytes
59    #[cfg_attr(feature = "serde", serde(with = "serialization::modules"))]
60    #[debug(
61        "{:?}",
62        modules
63            .iter()
64            .map(|m| <base64ct::Base64 as base64ct::Encoding>::encode_string(m))
65            .collect::<Vec<_>>()
66    )]
67    pub modules: Vec<Vec<u8>>,
68    /// The package dependencies, specified by their object IDs.
69    pub dependencies: Vec<ObjectId>,
70    /// The package digest.
71    #[cfg_attr(feature = "serde", serde(with = "serialization::digest"))]
72    pub digest: Digest,
73}
74
75impl MovePackageData {
76    #[cfg(feature = "hash")]
77    pub fn new(modules: Vec<Vec<u8>>, dependencies: Vec<ObjectId>) -> Self {
78        let digest = MovePackage::compute_digest_for_modules_and_deps(&modules, &dependencies);
79
80        Self {
81            modules,
82            dependencies,
83            digest,
84        }
85    }
86}
87
88impl crate::TreeDisplay for MovePackageData {
89    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
90        w.header("Move Package Data")?;
91        w.base64_leaves("Modules", &self.modules, false)?;
92        w.leaves("Dependencies", &self.dependencies, false)?;
93        w.leaf("Digest", &self.digest, true)
94    }
95}
96
97/// Upgraded package info for [MovePackage]'s linkage_table.
98///
99/// # BCS
100///
101/// The BCS serialized form for this type is defined by the following ABNF:
102///
103/// ```text
104/// upgrade-info = object-id version
105/// ```
106#[derive(Clone, Debug, Eq, Hash, PartialEq)]
107#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
108#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
109#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
110pub struct UpgradeInfo {
111    /// ID of the upgraded package
112    pub upgraded_id: ObjectId,
113    /// Version of the upgraded package
114    pub upgraded_version: Version,
115}
116
117impl crate::TreeDisplay for UpgradeInfo {
118    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
119        w.header("Upgrade Info")?;
120        w.leaf("Upgraded ID", &self.upgraded_id, false)?;
121        w.leaf("Upgraded Version", &self.upgraded_version, true)
122    }
123}
124
125/// Stores the origin of a data type where it first appeared in the version
126/// chain. A data type is identified by the name of the module and the name of
127/// the struct/enum in combination.
128///
129/// # BCS
130///
131/// The BCS serialized form for this type is defined by the following ABNF:
132///
133/// ```text
134/// type-origin = identifier identifier object-id
135/// ```
136#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
138#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
139#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
140pub struct TypeOrigin {
141    /// The name of the module the data type resides in.
142    pub module_name: Identifier,
143    /// The name of the data type. Either refers to an enum or a struct
144    /// identifier.
145    pub datatype_name: Identifier,
146    /// ID of the package, where the given type first appeared.
147    pub package: ObjectId,
148}
149
150impl crate::TreeDisplay for TypeOrigin {
151    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
152        w.header("Type Origin")?;
153        w.leaf("Module", &self.module_name, false)?;
154        w.leaf("Struct", &self.datatype_name, false)?;
155        w.leaf("Package", &self.package, true)
156    }
157}
158
159/// A move package
160///
161/// # BCS
162///
163/// The BCS serialized form for this type is defined by the following ABNF:
164///
165/// ```text
166/// move-package = object-id                          ; id
167///                version                            ; version
168///                (vector (identifier bytes))        ; modules
169///                (vector type-origin)               ; type-origin-table
170///                (vector (object-id upgrade-info))  ; linkage-table
171/// ```
172#[derive(Clone, derive_more::Debug, Eq, Hash, PartialEq)]
173#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
174#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
175#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
176pub struct MovePackage {
177    /// The ID of this package
178    pub id: ObjectId,
179    /// Most move packages are uniquely identified by their ID (i.e. there is
180    /// only one version per ID), but the version is still stored because
181    /// one package may be an upgrade of another (at a different ID), in
182    /// which case its version will be one greater than the version of the
183    /// upgraded package.
184    ///
185    /// Framework packages are an exception to this rule -- all versions of the
186    /// framework packages exist at the same ID, at increasing versions.
187    ///
188    /// In all cases, packages are referred to by move calls using just their
189    /// ID, and they are always loaded at their latest version.
190    pub version: Version,
191    /// Set of modules defined by this package
192    #[cfg_attr(
193        feature = "serde",
194        serde(
195            with = "::serde_with::As::<BTreeMap<::serde_with::Same, ::serde_with::IfIsHumanReadable<crate::_serde::Base64Encoded, ::serde_with::Bytes>>>"
196        )
197    )]
198    #[cfg_attr(
199        feature = "proptest",
200        strategy(
201            proptest::collection::btree_map(proptest::arbitrary::any::<Identifier>(), proptest::collection::vec(proptest::arbitrary::any::<u8>(), 0..=1024), 0..=5)
202        )
203    )]
204    #[debug(
205        "{:?}",
206        modules
207            .iter()
208            .map(|(k, v)| (k, <base64ct::Base64 as base64ct::Encoding>::encode_string(v)))
209            .collect::<BTreeMap<_, _>>()
210    )]
211    pub modules: BTreeMap<Identifier, Vec<u8>>,
212    /// Maps structs and enums in a given module to a package version where it
213    /// was first defined, stored as a vector for simple serialization and
214    /// deserialization.
215    pub type_origin_table: Vec<TypeOrigin>,
216    /// For each dependency, maps original package ID to the info about the
217    /// (upgraded) dependency version that this package is using.
218    #[cfg_attr(
219        feature = "proptest",
220        strategy(
221            proptest::collection::btree_map(proptest::arbitrary::any::<ObjectId>(), proptest::arbitrary::any::<UpgradeInfo>(), 0..=5)
222        )
223    )]
224    pub linkage_table: BTreeMap<ObjectId, UpgradeInfo>,
225}
226
227impl MovePackage {
228    /// Create a package with all required data (including serialized modules,
229    /// type origin and linkage tables) already supplied.
230    ///
231    /// It does not perform any type of validation. Ensure that the supplied
232    /// parts are semantically valid.
233    pub fn new(
234        id: ObjectId,
235        version: Version,
236        modules: BTreeMap<Identifier, Vec<u8>>,
237        max_move_package_size: u64,
238        type_origin_table: Vec<TypeOrigin>,
239        linkage_table: BTreeMap<ObjectId, UpgradeInfo>,
240    ) -> Result<Self, ExecutionError> {
241        let pkg = Self {
242            id,
243            version,
244            modules,
245            type_origin_table,
246            linkage_table,
247        };
248        let object_size = pkg.size() as u64;
249        if object_size > max_move_package_size {
250            return Err(ExecutionError::PackageTooBig {
251                object_size,
252                max_object_size: max_move_package_size,
253            });
254        }
255        Ok(pkg)
256    }
257
258    /// Calculate the digest of the [MovePackage].
259    #[cfg(feature = "hash")]
260    pub fn digest(&self) -> Digest {
261        Self::compute_digest_for_modules_and_deps(
262            self.modules.values(),
263            self.linkage_table
264                .values()
265                .map(|UpgradeInfo { upgraded_id, .. }| upgraded_id),
266        )
267    }
268
269    /// It is important that this function is shared across both the calculation
270    /// of the digest for the package, and the calculation of the digest
271    /// on-chain.
272    #[cfg(feature = "hash")]
273    pub fn compute_digest_for_modules_and_deps<'a>(
274        modules: impl IntoIterator<Item = &'a Vec<u8>>,
275        object_ids: impl IntoIterator<Item = &'a ObjectId>,
276    ) -> Digest {
277        let mut components = object_ids
278            .into_iter()
279            .map(|o| o.into_bytes())
280            .chain(
281                modules
282                    .into_iter()
283                    .map(|module| Hasher::digest(module).into_inner()),
284            )
285            .collect::<Vec<_>>();
286
287        // NB: sorting so the order of the modules and the order of the dependencies
288        // does not matter.
289        components.sort();
290
291        let mut digest = Hasher::new();
292        for c in components {
293            digest.update(c);
294        }
295        digest.finalize()
296    }
297
298    /// Retrieve the module from this package with the given [Identifier].
299    pub fn get_module(&self, name: &Identifier) -> Option<&Vec<u8>> {
300        self.modules.get(name)
301    }
302
303    /// Return the size of the package in bytes
304    pub fn size(&self) -> usize {
305        let module_map_size = self
306            .modules
307            .iter()
308            .map(|(name, module)| name.len() + module.len())
309            .sum::<usize>();
310        let type_origin_table_size = self
311            .type_origin_table
312            .iter()
313            .map(
314                |TypeOrigin {
315                     module_name,
316                     datatype_name,
317                     ..
318                 }| module_name.len() + datatype_name.len() + ObjectId::LENGTH,
319            )
320            .sum::<usize>();
321
322        let linkage_table_size = self.linkage_table.len()
323            * (ObjectId::LENGTH + (ObjectId::LENGTH + std::mem::size_of::<Version>()));
324
325        std::mem::size_of::<Version>()
326            + module_map_size
327            + type_origin_table_size
328            + linkage_table_size
329    }
330
331    /// `Package ID`/`Storage ID` of this package.
332    pub fn id(&self) -> ObjectId {
333        self.id
334    }
335
336    pub fn version(&self) -> Version {
337        self.version
338    }
339
340    pub fn increment_version(&mut self) -> Result<(), VersionError> {
341        self.version.increment()
342    }
343
344    pub fn decrement_version(&mut self) -> Result<(), VersionError> {
345        self.version.decrement()
346    }
347
348    pub fn serialized_module_map(&self) -> &BTreeMap<Identifier, Vec<u8>> {
349        &self.modules
350    }
351
352    pub fn type_origin_table(&self) -> &Vec<TypeOrigin> {
353        &self.type_origin_table
354    }
355
356    pub fn type_origin_map(&self) -> BTreeMap<(Identifier, Identifier), ObjectId> {
357        self.type_origin_table
358            .iter()
359            .map(
360                |TypeOrigin {
361                     module_name,
362                     datatype_name,
363                     package,
364                 }| { ((module_name.clone(), datatype_name.clone()), *package) },
365            )
366            .collect()
367    }
368
369    pub fn linkage_table(&self) -> &BTreeMap<ObjectId, UpgradeInfo> {
370        &self.linkage_table
371    }
372}
373
374impl crate::TreeDisplay for MovePackage {
375    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
376        w.header("Move Package")?;
377        w.leaf("ID", &self.id, false)?;
378        w.leaf("Version", &self.version, false)?;
379        w.leaves("Modules", self.modules.keys(), true)
380    }
381}
382
383crate::impl_tree_display!(MovePackageData, UpgradeInfo, TypeOrigin, MovePackage);
384
385#[cfg(feature = "serde")]
386mod serialization {
387    use base64ct::Encoding;
388    use serde::{Deserialize, Deserializer, Serialize, Serializer};
389
390    use super::*;
391
392    impl MovePackageData {
393        pub fn to_base64(&self) -> String {
394            base64ct::Base64::encode_string(&bcs::to_bytes(self).expect("bcs encoding failed"))
395        }
396
397        pub fn from_base64(base64: &str) -> Result<Self, bcs::Error> {
398            use serde::de::Error;
399            bcs::from_bytes(&base64ct::Base64::decode_vec(base64).map_err(bcs::Error::custom)?)
400        }
401    }
402
403    pub mod modules {
404        use super::*;
405
406        pub fn serialize<S: Serializer>(
407            value: &[Vec<u8>],
408            serializer: S,
409        ) -> Result<S::Ok, S::Error> {
410            value
411                .iter()
412                .map(|v| base64ct::Base64::encode_string(v))
413                .collect::<Vec<_>>()
414                .serialize(serializer)
415        }
416
417        pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
418        where
419            D: Deserializer<'de>,
420        {
421            let bcs = Vec::<String>::deserialize(deserializer)?;
422            bcs.into_iter()
423                .map(|s| base64ct::Base64::decode_vec(&s).map_err(serde::de::Error::custom))
424                .collect()
425        }
426    }
427
428    pub mod digest {
429        use super::*;
430
431        pub fn serialize<S: Serializer>(value: &Digest, serializer: S) -> Result<S::Ok, S::Error> {
432            value.as_bytes().serialize(serializer)
433        }
434
435        pub fn deserialize<'de, D>(deserializer: D) -> Result<Digest, D::Error>
436        where
437            D: Deserializer<'de>,
438        {
439            let bytes = Vec::<u8>::deserialize(deserializer)?;
440            Digest::from_bytes(bytes).map_err(|e| serde::de::Error::custom(format!("{e}")))
441        }
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[cfg(feature = "serde")]
450    const PACKAGE: &str = r#"{"modules":["oRzrCwYAAAAKAQAIAggUAxw+BFoGBWBBB6EBwQEI4gJACqIDGgy8A5cBDdMEBgAKAQ0BEwEUAAIMAAABCAAAAAgAAQQEAAMDAgAACAABAAAJAgMAABACAwAAEgQDAAAMBQYAAAYHAQAAEQgBAAAFCQoAAQsACwACDg8BAQwCEw8BAQgDDwwNAAoOCgYJBgEHCAQAAQYIAAEDAQYIAQQHCAEDAwcIBAEIAAQDAwUHCAQDCAAFBwgEAgMHCAQBCAIBCAMBBggEAQUBCAECCQAFBkNvbmZpZwVGb3JnZQVTd29yZAlUeENvbnRleHQDVUlEDWNyZWF0ZV9jb25maWcMY3JlYXRlX3N3b3JkAmlkBGluaXQFbWFnaWMJbXlfbW9kdWxlA25ldwluZXdfc3dvcmQGb2JqZWN0D3B1YmxpY190cmFuc2ZlcgZzZW5kZXIIc3RyZW5ndGgOc3dvcmRfdHJhbnNmZXIOc3dvcmRzX2NyZWF0ZWQIdHJhbnNmZXIKdHhfY29udGV4dAV2YWx1ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgMHCAMJAxADAQICBwgDEgMCAgIHCAMVAwAAAAABCQoAEQgGAAAAAAAAAAASAQsALhELOAACAQEAAAEECwAQABQCAgEAAAEECwAQARQCAwEAAAEECwAQAhQCBAEAAAEOCgAQAhQGAQAAAAAAAAAWCwAPAhULAxEICwELAhIAAgUBAAABCAsDEQgLAAsBEgALAjgBAgYBAAABBAsACwE4AgIHAQAAAQULAREICwASAgIAAQACAQEA"],"dependencies":["0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000001"],"digest":[246,127,102,77,186,19,68,12,161,181,56,248,210,0,91,211,245,251,165,152,0,197,250,135,171,37,177,240,133,76,122,124]}"#;
451
452    #[cfg(feature = "serde")]
453    #[test]
454    fn test_serialization() {
455        let package: MovePackageData = serde_json::from_str(PACKAGE).unwrap();
456        let new_json = serde_json::to_string(&package).unwrap();
457        assert_eq!(new_json, PACKAGE);
458    }
459
460    #[cfg(all(feature = "serde", feature = "hash"))]
461    #[test]
462    fn test_digest() {
463        let json_package: MovePackageData = serde_json::from_str(PACKAGE).unwrap();
464        let package = MovePackageData::new(json_package.modules, json_package.dependencies);
465        assert_eq!(json_package.digest, package.digest);
466    }
467
468    fn module(name: &str, bytes: &[u8]) -> (Identifier, Vec<u8>) {
469        (Identifier::new(name).unwrap(), bytes.to_vec())
470    }
471
472    fn upgrade_info(upgraded_id: ObjectId, version: u64) -> UpgradeInfo {
473        UpgradeInfo {
474            upgraded_id,
475            upgraded_version: Version::from_u64(version),
476        }
477    }
478
479    fn package(
480        modules: impl IntoIterator<Item = (Identifier, Vec<u8>)>,
481        type_origin_table: Vec<TypeOrigin>,
482        linkage_table: impl IntoIterator<Item = (ObjectId, UpgradeInfo)>,
483    ) -> MovePackage {
484        MovePackage {
485            id: ObjectId::ZERO,
486            version: Version::OBJECT_START,
487            modules: modules.into_iter().collect(),
488            type_origin_table,
489            linkage_table: linkage_table.into_iter().collect(),
490        }
491    }
492
493    #[test]
494    fn size_all_components() {
495        let mod_a_name = "m1";
496        let mod_a_bytes: &[u8] = &[0; 10];
497        let mod_b_name = "m2";
498        let mod_b_bytes: &[u8] = &[0; 20];
499        let type_module_name = "t_module";
500        let type_datatype_name = "T";
501
502        let pkg = package(
503            [
504                module(mod_a_name, mod_a_bytes),
505                module(mod_b_name, mod_b_bytes),
506            ],
507            vec![TypeOrigin {
508                module_name: Identifier::new(type_module_name).unwrap(),
509                datatype_name: Identifier::new(type_datatype_name).unwrap(),
510                package: ObjectId::ZERO,
511            }],
512            [(
513                ObjectId::from_u16(1),
514                upgrade_info(ObjectId::from_u16(2), 1),
515            )],
516        );
517        let modules_size =
518            (mod_a_name.len() + mod_a_bytes.len()) + (mod_b_name.len() + mod_b_bytes.len());
519        let type_origin_size = type_module_name.len() + type_datatype_name.len() + ObjectId::LENGTH;
520        let linkage_size = ObjectId::LENGTH + ObjectId::LENGTH + std::mem::size_of::<Version>();
521        let expected =
522            std::mem::size_of::<Version>() + modules_size + type_origin_size + linkage_size;
523        assert_eq!(pkg.size(), expected);
524    }
525
526    #[test]
527    fn new_exceeding_max_size_fails() {
528        let modules = [module("m", &[0; 100])];
529        let max = 10_u64;
530        let err = MovePackage::new(
531            ObjectId::ZERO,
532            Version::OBJECT_START,
533            modules.into_iter().collect(),
534            max,
535            vec![],
536            BTreeMap::new(),
537        )
538        .unwrap_err();
539        match err {
540            ExecutionError::PackageTooBig {
541                object_size,
542                max_object_size,
543            } => {
544                assert!(object_size > max);
545                assert_eq!(max_object_size, max);
546            }
547            other => panic!("expected PackageTooBig, got {other:?}"),
548        }
549    }
550
551    #[cfg(feature = "hash")]
552    mod digest {
553        use super::*;
554
555        #[test]
556        fn digest_matches_pinned_hex() {
557            const EXPECTED_HEX: &str =
558                "087322ae613945320a83dce5d45626eb3a112a0d98ed6885788635194f2d566f";
559
560            let pkg = package(
561                [module("a", b"first"), module("b", b"second")],
562                vec![],
563                [(
564                    ObjectId::from_u16(0x01),
565                    upgrade_info(ObjectId::from_u16(0xdead), 1),
566                )],
567            );
568
569            let expected = Digest::new(hex::decode(EXPECTED_HEX).unwrap().try_into().unwrap());
570            assert_eq!(pkg.digest(), expected);
571        }
572
573        #[test]
574        fn digest_ignores_module_names() {
575            // Same module bytes under different identifier keys must hash the same,
576            // because the digest is computed over module *values* only.
577            let bytes = b"module_bytes".to_vec();
578            let pkg1 = package(
579                [(Identifier::new("foo").unwrap(), bytes.clone())],
580                vec![],
581                [],
582            );
583            let pkg2 = package([(Identifier::new("bar").unwrap(), bytes)], vec![], []);
584            assert_eq!(pkg1.digest(), pkg2.digest());
585        }
586
587        #[test]
588        fn digest_changes_with_module_bytes() {
589            let pkg1 = package([module("m", b"version_one")], vec![], []);
590            let pkg2 = package([module("m", b"version_two")], vec![], []);
591            assert_ne!(pkg1.digest(), pkg2.digest());
592        }
593
594        #[test]
595        fn digest_changes_with_dependencies() {
596            let modules = [module("m", b"same_bytes")];
597            let pkg1 = package(modules.clone(), vec![], []);
598            let pkg2 = package(
599                modules,
600                vec![],
601                [(
602                    ObjectId::from_u16(0x01),
603                    upgrade_info(ObjectId::from_u16(0x99), 1),
604                )],
605            );
606            assert_ne!(pkg1.digest(), pkg2.digest());
607        }
608
609        #[test]
610        fn digest_uses_only_upgraded_id_from_linkage() {
611            let modules = [module("m", b"bytes")];
612            let upgraded = ObjectId::from_u16(0xaa);
613
614            // Different linkage-table keys (original_id) and versions, same upgraded_id
615            // → digest must be unchanged.
616            let pkg1 = package(
617                modules.clone(),
618                vec![],
619                [(ObjectId::from_u16(0x01), upgrade_info(upgraded, 1))],
620            );
621            let pkg2 = package(
622                modules.clone(),
623                vec![],
624                [(ObjectId::from_u16(0x02), upgrade_info(upgraded, 999))],
625            );
626            assert_eq!(pkg1.digest(), pkg2.digest());
627
628            // Same original_id, different upgraded_id → digest must change.
629            let pkg3 = package(
630                modules,
631                vec![],
632                [(
633                    ObjectId::from_u16(0x01),
634                    upgrade_info(ObjectId::from_u16(0xbb), 1),
635                )],
636            );
637            assert_ne!(pkg1.digest(), pkg3.digest());
638        }
639    }
640}