Skip to main content

sui_sdk_types/
hash.rs

1use crate::Address;
2use crate::Digest;
3
4use blake2::Digest as DigestTrait;
5
6type Blake2b256 = blake2::Blake2b<blake2::digest::consts::U32>;
7
8/// A Blake2b256 Hasher
9#[derive(Debug, Default)]
10pub struct Hasher(Blake2b256);
11
12impl Hasher {
13    /// Initialize a new Blake2b256 Hasher instance.
14    pub fn new() -> Self {
15        Self(Blake2b256::new())
16    }
17
18    /// Process the provided data, updating internal state.
19    pub fn update<T: AsRef<[u8]>>(&mut self, data: T) {
20        self.0.update(data)
21    }
22
23    /// Finalize hashing, consuming the Hasher instance and returning the resultant hash or
24    /// `Digest`.
25    pub fn finalize(self) -> Digest {
26        let mut buf = [0; Digest::LENGTH];
27        let result = self.0.finalize();
28
29        buf.copy_from_slice(result.as_ref());
30
31        Digest::new(buf)
32    }
33
34    /// Convenience function for creating a new Hasher instance, hashing the provided data, and
35    /// returning the resultant `Digest`
36    pub fn digest<T: AsRef<[u8]>>(data: T) -> Digest {
37        let mut hasher = Self::new();
38        hasher.update(data);
39        hasher.finalize()
40    }
41}
42
43impl std::io::Write for Hasher {
44    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
45        self.0.write(buf)
46    }
47
48    fn flush(&mut self) -> std::io::Result<()> {
49        self.0.flush()
50    }
51}
52
53impl crate::Ed25519PublicKey {
54    /// Derive an `Address` from this Public Key
55    ///
56    /// An `Address` can be derived from an `Ed25519PublicKey` by hashing the bytes of the public
57    /// key prefixed with the Ed25519 `SignatureScheme` flag (`0x00`).
58    ///
59    /// `hash( 0x00 || 32-byte ed25519 public key)`
60    ///
61    /// ```
62    /// use sui_sdk_types::Address;
63    /// use sui_sdk_types::Ed25519PublicKey;
64    /// use sui_sdk_types::hash::Hasher;
65    ///
66    /// let public_key_bytes = [0; 32];
67    /// let mut hasher = Hasher::new();
68    /// hasher.update([0x00]); // The SignatureScheme flag for Ed25519 is `0`
69    /// hasher.update(public_key_bytes);
70    /// let address = Address::new(hasher.finalize().into_inner());
71    /// println!("Address: {}", address);
72    ///
73    /// let public_key = Ed25519PublicKey::new(public_key_bytes);
74    /// assert_eq!(address, public_key.derive_address());
75    /// ```
76    pub fn derive_address(&self) -> Address {
77        let mut hasher = Hasher::new();
78        self.write_into_hasher(&mut hasher);
79        let digest = hasher.finalize();
80        Address::new(digest.into_inner())
81    }
82
83    fn write_into_hasher(&self, hasher: &mut Hasher) {
84        hasher.update([self.scheme().to_u8()]);
85        hasher.update(self.inner());
86    }
87}
88
89impl crate::Secp256k1PublicKey {
90    /// Derive an `Address` from this Public Key
91    ///
92    /// An `Address` can be derived from a `Secp256k1PublicKey` by hashing the bytes of the public
93    /// key prefixed with the Secp256k1 `SignatureScheme` flag (`0x01`).
94    ///
95    /// `hash( 0x01 || 33-byte secp256k1 public key)`
96    ///
97    /// ```
98    /// use sui_sdk_types::Address;
99    /// use sui_sdk_types::Secp256k1PublicKey;
100    /// use sui_sdk_types::hash::Hasher;
101    ///
102    /// let public_key_bytes = [0; 33];
103    /// let mut hasher = Hasher::new();
104    /// hasher.update([0x01]); // The SignatureScheme flag for Secp256k1 is `1`
105    /// hasher.update(public_key_bytes);
106    /// let address = Address::new(hasher.finalize().into_inner());
107    /// println!("Address: {}", address);
108    ///
109    /// let public_key = Secp256k1PublicKey::new(public_key_bytes);
110    /// assert_eq!(address, public_key.derive_address());
111    /// ```
112    pub fn derive_address(&self) -> Address {
113        let mut hasher = Hasher::new();
114        self.write_into_hasher(&mut hasher);
115        let digest = hasher.finalize();
116        Address::new(digest.into_inner())
117    }
118
119    fn write_into_hasher(&self, hasher: &mut Hasher) {
120        hasher.update([self.scheme().to_u8()]);
121        hasher.update(self.inner());
122    }
123}
124
125impl crate::Secp256r1PublicKey {
126    /// Derive an `Address` from this Public Key
127    ///
128    /// An `Address` can be derived from a `Secp256r1PublicKey` by hashing the bytes of the public
129    /// key prefixed with the Secp256r1 `SignatureScheme` flag (`0x02`).
130    ///
131    /// `hash( 0x02 || 33-byte secp256r1 public key)`
132    ///
133    /// ```
134    /// use sui_sdk_types::Address;
135    /// use sui_sdk_types::Secp256r1PublicKey;
136    /// use sui_sdk_types::hash::Hasher;
137    ///
138    /// let public_key_bytes = [0; 33];
139    /// let mut hasher = Hasher::new();
140    /// hasher.update([0x02]); // The SignatureScheme flag for Secp256r1 is `2`
141    /// hasher.update(public_key_bytes);
142    /// let address = Address::new(hasher.finalize().into_inner());
143    /// println!("Address: {}", address);
144    ///
145    /// let public_key = Secp256r1PublicKey::new(public_key_bytes);
146    /// assert_eq!(address, public_key.derive_address());
147    /// ```
148    pub fn derive_address(&self) -> Address {
149        let mut hasher = Hasher::new();
150        self.write_into_hasher(&mut hasher);
151        let digest = hasher.finalize();
152        Address::new(digest.into_inner())
153    }
154
155    fn write_into_hasher(&self, hasher: &mut Hasher) {
156        hasher.update([self.scheme().to_u8()]);
157        hasher.update(self.inner());
158    }
159}
160
161impl crate::ZkLoginPublicIdentifier {
162    /// Derive an `Address` from this `ZkLoginPublicIdentifier` by hashing the byte length of the
163    /// `iss` followed by the `iss` bytes themselves and the full 32 byte `address_seed` value, all
164    /// prefixed with the zklogin `SignatureScheme` flag (`0x05`).
165    ///
166    /// `hash( 0x05 || iss_bytes_len || iss_bytes || 32_byte_address_seed )`
167    pub fn derive_address_padded(&self) -> Address {
168        let mut hasher = Hasher::new();
169        self.write_into_hasher_padded(&mut hasher);
170        let digest = hasher.finalize();
171        Address::new(digest.into_inner())
172    }
173
174    fn write_into_hasher_padded(&self, hasher: &mut Hasher) {
175        hasher.update([self.scheme().to_u8()]);
176        hasher.update([self.iss().len() as u8]); // TODO enforce iss is less than 255 bytes
177        hasher.update(self.iss());
178        hasher.update(self.address_seed().padded());
179    }
180
181    /// Derive an `Address` from this `ZkLoginPublicIdentifier` by hashing the byte length of the
182    /// `iss` followed by the `iss` bytes themselves and the `address_seed` bytes with any leading
183    /// zero-bytes stripped, all prefixed with the zklogin `SignatureScheme` flag (`0x05`).
184    ///
185    /// `hash( 0x05 || iss_bytes_len || iss_bytes || unpadded_32_byte_address_seed )`
186    pub fn derive_address_unpadded(&self) -> Address {
187        let mut hasher = Hasher::new();
188        hasher.update([self.scheme().to_u8()]);
189        hasher.update([self.iss().len() as u8]); // TODO enforce iss is less than 255 bytes
190        hasher.update(self.iss());
191        hasher.update(self.address_seed().unpadded());
192        let digest = hasher.finalize();
193        Address::new(digest.into_inner())
194    }
195
196    /// Provides an iterator over the addresses that correspond to this zklogin authenticator.
197    ///
198    /// In the majority of instances this will only yield a single address, except for the
199    /// instances where the `address_seed` value has a leading zero-byte, in such cases the
200    /// returned iterator will yield two addresses.
201    pub fn derive_address(&self) -> impl ExactSizeIterator<Item = Address> {
202        self.internal_derive_addresses()
203    }
204
205    // Private internal function
206    fn internal_derive_addresses(&self) -> DerivedAddressIter {
207        let primary = self.derive_address_padded();
208        let mut addresses = DerivedAddressIter::new(primary);
209
210        // If address_seed starts with a zero byte then we know that this zklogin authenticator has
211        // two addresses
212        if self.address_seed().padded()[0] == 0 {
213            let secondary_address = self.derive_address_unpadded();
214
215            addresses.extra = Some(secondary_address);
216        }
217
218        addresses
219    }
220}
221
222impl crate::ZkLoginAuthenticator {
223    pub fn derive_address_padded(&self) -> Address {
224        self.inputs.public_identifier().derive_address_padded()
225    }
226
227    pub fn derive_address_unpadded(&self) -> Address {
228        self.inputs.public_identifier().derive_address_unpadded()
229    }
230
231    pub fn derive_address(&self) -> impl ExactSizeIterator<Item = Address> {
232        self.inputs.public_identifier().derive_address()
233    }
234}
235
236impl crate::MlDsa65PublicKey {
237    /// Derive an `Address` from this Public Key
238    ///
239    /// `hash( 0x07 || 1952-byte ml-dsa-65 public key)`
240    pub fn derive_address(&self) -> Address {
241        let mut hasher = Hasher::new();
242        self.write_into_hasher(&mut hasher);
243        let digest = hasher.finalize();
244        Address::new(digest.into_inner())
245    }
246
247    fn write_into_hasher(&self, hasher: &mut Hasher) {
248        hasher.update([self.scheme().to_u8()]);
249        hasher.update(self.inner());
250    }
251}
252
253impl crate::PasskeyPublicKey {
254    /// Derive an `Address` from this Passkey Public Key
255    ///
256    /// An `Address` can be derived from a `PasskeyPublicKey` by hashing the bytes of the
257    /// `Secp256r1PublicKey` that corresponds to this passkey prefixed with the Passkey
258    /// `SignatureScheme` flag (`0x06`).
259    ///
260    /// `hash( 0x06 || 33-byte secp256r1-public-key)`
261    pub fn derive_address(&self) -> Address {
262        let mut hasher = Hasher::new();
263        self.write_into_hasher(&mut hasher);
264        let digest = hasher.finalize();
265        Address::new(digest.into_inner())
266    }
267
268    fn write_into_hasher(&self, hasher: &mut Hasher) {
269        hasher.update([self.scheme().to_u8()]);
270        hasher.update(self.inner().inner());
271    }
272}
273
274impl crate::PasskeyAuthenticator {
275    pub fn derive_address(&self) -> Address {
276        self.public_key().derive_address()
277    }
278}
279
280impl crate::MultisigCommittee {
281    /// Derive an `Address` from this MultisigCommittee.
282    ///
283    /// A MultiSig address
284    /// is defined as the 32-byte Blake2b hash of serializing the `SignatureScheme` flag (0x03), the
285    /// threshold (in little endian), and the concatenation of all n flag, public keys and
286    /// its weight.
287    ///
288    /// `hash(0x03 || threshold || flag_1 || pk_1 || weight_1
289    /// || ... || flag_n || pk_n || weight_n)`.
290    ///
291    /// When flag_i is ZkLogin, the pk_i for the [`ZkLoginPublicIdentifier`] refers to the same
292    /// input used when deriving the address using the
293    /// [`ZkLoginPublicIdentifier::derive_address_padded`] method (using the full 32-byte
294    /// `address_seed` value).
295    ///
296    /// [`ZkLoginPublicIdentifier`]: crate::ZkLoginPublicIdentifier
297    /// [`ZkLoginPublicIdentifier::derive_address_padded`]: crate::ZkLoginPublicIdentifier::derive_address_padded
298    pub fn derive_address(&self) -> Address {
299        use crate::MultisigMemberPublicKey::*;
300
301        let mut hasher = Hasher::new();
302        hasher.update([self.scheme().to_u8()]);
303        hasher.update(self.threshold().to_le_bytes());
304
305        for member in self.members() {
306            match member.public_key() {
307                Ed25519(p) => p.write_into_hasher(&mut hasher),
308                Secp256k1(p) => p.write_into_hasher(&mut hasher),
309                Secp256r1(p) => p.write_into_hasher(&mut hasher),
310                ZkLogin(p) => p.write_into_hasher_padded(&mut hasher),
311                Passkey(p) => p.write_into_hasher(&mut hasher),
312                MlDsa65(p) => p.write_into_hasher(&mut hasher),
313            }
314
315            hasher.update(member.weight().to_le_bytes());
316        }
317
318        let digest = hasher.finalize();
319        Address::new(digest.into_inner())
320    }
321}
322
323impl crate::MultisigAggregatedSignature {
324    pub fn derive_address(&self) -> Address {
325        self.committee().derive_address()
326    }
327}
328
329impl crate::SimpleSignature {
330    pub fn derive_address(&self) -> Address {
331        match self {
332            crate::SimpleSignature::Ed25519 { public_key, .. } => public_key.derive_address(),
333            crate::SimpleSignature::Secp256k1 { public_key, .. } => public_key.derive_address(),
334            crate::SimpleSignature::Secp256r1 { public_key, .. } => public_key.derive_address(),
335            crate::SimpleSignature::MlDsa65 { public_key, .. } => public_key.derive_address(),
336        }
337    }
338}
339
340impl crate::UserSignature {
341    pub fn derive_address(&self) -> Address {
342        self.derive_addresses().next().unwrap()
343    }
344
345    pub fn derive_addresses(&self) -> impl ExactSizeIterator<Item = Address> {
346        match self {
347            crate::UserSignature::Simple(simple) => {
348                DerivedAddressIter::new(simple.derive_address())
349            }
350            crate::UserSignature::Multisig(multisig) => {
351                DerivedAddressIter::new(multisig.derive_address())
352            }
353            crate::UserSignature::ZkLogin(zk) => {
354                zk.inputs.public_identifier().internal_derive_addresses()
355            }
356            crate::UserSignature::Passkey(passkey) => {
357                DerivedAddressIter::new(passkey.derive_address())
358            }
359        }
360    }
361}
362
363struct DerivedAddressIter {
364    primary: Option<Address>,
365    extra: Option<Address>,
366}
367
368impl DerivedAddressIter {
369    fn new(primary: Address) -> Self {
370        Self {
371            primary: Some(primary),
372            extra: None,
373        }
374    }
375}
376
377impl ExactSizeIterator for DerivedAddressIter {}
378impl Iterator for DerivedAddressIter {
379    type Item = Address;
380
381    fn next(&mut self) -> Option<Self::Item> {
382        if self.primary.is_some() {
383            self.primary.take()
384        } else if self.extra.is_some() {
385            self.extra.take()
386        } else {
387            None
388        }
389    }
390
391    fn size_hint(&self) -> (usize, Option<usize>) {
392        let len = self.primary.iter().len() + self.extra.iter().len();
393        (len, Some(len))
394    }
395}
396
397#[cfg(feature = "serde")]
398#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
399mod type_digest {
400    use super::Hasher;
401    use crate::Digest;
402
403    impl crate::Object {
404        /// Calculate the digest of this `Object`
405        ///
406        /// This is done by hashing the BCS bytes of this `Object` prefixed
407        pub fn digest(&self) -> Digest {
408            const SALT: &str = "Object::";
409            type_digest(SALT, self)
410        }
411    }
412
413    impl crate::CheckpointSummary {
414        pub fn digest(&self) -> Digest {
415            const SALT: &str = "CheckpointSummary::";
416            type_digest(SALT, self)
417        }
418    }
419
420    impl crate::CheckpointContents {
421        pub fn digest(&self) -> Digest {
422            const SALT: &str = "CheckpointContents::";
423            type_digest(SALT, self)
424        }
425    }
426
427    impl crate::Transaction {
428        pub fn digest(&self) -> Digest {
429            const SALT: &str = "TransactionData::";
430            type_digest(SALT, self)
431        }
432    }
433
434    impl crate::TransactionEffects {
435        pub fn digest(&self) -> Digest {
436            const SALT: &str = "TransactionEffects::";
437            type_digest(SALT, self)
438        }
439    }
440
441    impl crate::TransactionEvents {
442        pub fn digest(&self) -> Digest {
443            const SALT: &str = "TransactionEvents::";
444            type_digest(SALT, self)
445        }
446    }
447
448    impl crate::Event {
449        /// Per-event digest used as the leaf input for authenticated event
450        /// stream commitments.
451        ///
452        /// Unlike the [`TransactionEvents`] digest above, this is **not**
453        /// salted with the type name: the framework computes it as the bare
454        /// `BLAKE2b-256` over the BCS-encoded `Event`. Two events with
455        /// identical contents share a digest; their merkle leaves remain
456        /// distinct because the surrounding `EventCommitment` also pins each
457        /// event to its `(checkpoint_seq, transaction_idx, event_idx)`
458        /// position.
459        ///
460        /// [`TransactionEvents`]: crate::TransactionEvents
461        pub fn digest(&self) -> Digest {
462            let mut hasher = Hasher::new();
463            bcs::serialize_into(&mut hasher, self).unwrap();
464            hasher.finalize()
465        }
466    }
467
468    fn type_digest<T: serde::Serialize>(salt: &str, ty: &T) -> Digest {
469        let mut hasher = Hasher::new();
470        hasher.update(salt);
471        bcs::serialize_into(&mut hasher, ty).unwrap();
472        hasher.finalize()
473    }
474}
475
476#[cfg(feature = "serde")]
477#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
478mod signing_message {
479    use crate::Digest;
480    use crate::Intent;
481    use crate::IntentAppId;
482    use crate::IntentScope;
483    use crate::IntentVersion;
484    use crate::PersonalMessage;
485    use crate::SigningDigest;
486    use crate::Transaction;
487    use crate::hash::Hasher;
488
489    impl Transaction {
490        pub fn signing_digest(&self) -> SigningDigest {
491            const INTENT: Intent = Intent {
492                scope: IntentScope::TransactionData,
493                version: IntentVersion::V0,
494                app_id: IntentAppId::Sui,
495            };
496            let digest = signing_digest(INTENT, self);
497            digest.into_inner()
498        }
499    }
500
501    fn signing_digest<T: serde::Serialize + ?Sized>(intent: Intent, ty: &T) -> Digest {
502        let mut hasher = Hasher::new();
503        hasher.update(intent.to_bytes());
504        bcs::serialize_into(&mut hasher, ty).unwrap();
505        hasher.finalize()
506    }
507
508    impl PersonalMessage<'_> {
509        pub fn signing_digest(&self) -> SigningDigest {
510            const INTENT: Intent = Intent {
511                scope: IntentScope::PersonalMessage,
512                version: IntentVersion::V0,
513                app_id: IntentAppId::Sui,
514            };
515            let digest = signing_digest(INTENT, &self.0);
516            digest.into_inner()
517        }
518    }
519
520    impl crate::CheckpointSummary {
521        pub fn signing_message(&self) -> Vec<u8> {
522            const INTENT: Intent = Intent {
523                scope: IntentScope::CheckpointSummary,
524                version: IntentVersion::V0,
525                app_id: IntentAppId::Sui,
526            };
527            let mut message = Vec::new();
528            message.extend(INTENT.to_bytes());
529            bcs::serialize_into(&mut message, self).unwrap();
530            bcs::serialize_into(&mut message, &self.epoch).unwrap();
531            message
532        }
533    }
534}
535
536/// A 1-byte domain separator for deriving `ObjectId`s in Sui. It is starting from `0xf0` to ensure
537/// no hashing collision for any ObjectId vs Address which is derived as the hash of `flag ||
538/// pubkey`.
539#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
540#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
541#[repr(u8)]
542enum HashingIntent {
543    #[cfg(feature = "serde")]
544    ChildObjectId = 0xf0,
545    RegularObjectId = 0xf1,
546}
547
548impl crate::Address {
549    /// Create an ObjectId from `TransactionDigest` and `count`.
550    ///
551    /// `count` is the number of objects that have been created during a transactions.
552    pub fn derive_id(digest: crate::Digest, count: u64) -> Self {
553        let mut hasher = Hasher::new();
554        hasher.update([HashingIntent::RegularObjectId as u8]);
555        hasher.update(digest);
556        hasher.update(count.to_le_bytes());
557        let digest = hasher.finalize();
558        Self::new(digest.into_inner())
559    }
560
561    /// Derive an ObjectId for a Dynamic Child Object.
562    ///
563    /// hash(parent || len(key) || key || key_type_tag)
564    #[cfg(feature = "serde")]
565    #[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
566    pub fn derive_dynamic_child_id(&self, key_type_tag: &crate::TypeTag, key_bytes: &[u8]) -> Self {
567        let mut hasher = Hasher::new();
568        hasher.update([HashingIntent::ChildObjectId as u8]);
569        hasher.update(self);
570        hasher.update(
571            u64::try_from(key_bytes.len())
572                .expect("key_bytes must fit into a u64")
573                .to_le_bytes(),
574        );
575        hasher.update(key_bytes);
576        bcs::serialize_into(&mut hasher, key_type_tag)
577            .expect("bcs serialization of `TypeTag` cannot fail");
578        let digest = hasher.finalize();
579
580        Self::new(digest.into_inner())
581    }
582
583    /// Derive the address of a `derived_object`
584    ///
585    /// hash(parent || len(key) || key || DerivedObjectKey(key_type_tag))
586    #[cfg(feature = "serde")]
587    #[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
588    pub fn derive_object_id(&self, key_type_tag: &crate::TypeTag, key_bytes: &[u8]) -> Self {
589        use crate::Identifier;
590        use crate::StructTag;
591
592        let struct_tag = StructTag::new(
593            Address::from_static("0x2"),
594            Identifier::from_static("derived_object"),
595            Identifier::from_static("DerivedObjectKey"),
596            vec![key_type_tag.clone()],
597        );
598
599        self.derive_dynamic_child_id(&struct_tag.into(), key_bytes)
600    }
601}
602
603#[cfg(test)]
604mod test {
605    use super::HashingIntent;
606    use crate::Address;
607    use crate::SignatureScheme;
608    use crate::TypeTag;
609    use test_strategy::proptest;
610
611    #[cfg(target_arch = "wasm32")]
612    use wasm_bindgen_test::wasm_bindgen_test as test;
613
614    impl HashingIntent {
615        fn from_byte(byte: u8) -> Result<Self, u8> {
616            match byte {
617                0xf0 => Ok(Self::ChildObjectId),
618                0xf1 => Ok(Self::RegularObjectId),
619                invalid => Err(invalid),
620            }
621        }
622    }
623
624    #[proptest]
625    fn hashing_intent_does_not_overlap_with_signature_scheme(intent: HashingIntent) {
626        SignatureScheme::from_byte(intent as u8).unwrap_err();
627    }
628
629    #[proptest]
630    fn signature_scheme_does_not_overlap_with_hashing_intent(scheme: SignatureScheme) {
631        HashingIntent::from_byte(scheme.to_u8()).unwrap_err();
632    }
633
634    #[proptest]
635    fn roundtrip_hashing_intent(intent: HashingIntent) {
636        assert_eq!(Ok(intent), HashingIntent::from_byte(intent as u8));
637    }
638
639    // `Event::digest` must hash the bare BCS encoding without a salt — the
640    // framework relies on the unsalted digest as the leaf input for
641    // authenticated event stream commitments. If anyone reaches for the
642    // salted `type_digest` helper later by mistake, this test catches it.
643    #[test]
644    #[cfg(feature = "serde")]
645    fn event_digest_is_unsalted() {
646        use crate::Event;
647        use crate::Identifier;
648        use crate::StructTag;
649        use crate::hash::Hasher;
650
651        let event = Event {
652            package_id: Address::TWO,
653            module: Identifier::from_static("clock"),
654            sender: Address::TWO,
655            type_: StructTag::new(
656                Address::TWO,
657                Identifier::from_static("clock"),
658                Identifier::from_static("Tick"),
659                vec![],
660            ),
661            contents: vec![0xde, 0xad, 0xbe, 0xef],
662        };
663
664        let expected = Hasher::digest(bcs::to_bytes(&event).unwrap());
665        assert_eq!(event.digest(), expected);
666    }
667
668    // Snapshot tests that match the on-chain `derive_address` logic.
669    // These snapshots can also be found in `derived_object_tests.move` unit tests.
670    #[test]
671    #[cfg(feature = "serde")]
672    fn test_derive_object_snapshot() {
673        // Our key is `UID, Vec<u8>, b"foo"`
674        let key_bytes = bcs::to_bytes("foo").unwrap();
675        let key_type_tag = TypeTag::Vector(Box::new(TypeTag::U8));
676
677        let id = Address::from_hex("0x2")
678            .unwrap()
679            .derive_object_id(&key_type_tag, &key_bytes);
680
681        assert_eq!(
682            id,
683            Address::from_hex("0xa2b411aa9588c398d8e3bc97dddbdd430b5ded7f81545d05e33916c3ca0f30c3")
684                .unwrap()
685        );
686    }
687}