Skip to main content

iota_sdk_types/
hash.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use blake2::Digest as DigestTrait;
6
7use crate::{Address, Digest, PublicKeyExt};
8
9type Blake2b256 = blake2::Blake2b<blake2::digest::consts::U32>;
10
11/// A Blake2b256 Hasher
12#[derive(Debug, Default)]
13pub struct Hasher(Blake2b256);
14
15impl Hasher {
16    /// Initialize a new Blake2b256 Hasher instance.
17    pub fn new() -> Self {
18        Self(Blake2b256::new())
19    }
20
21    /// Process the provided data, updating internal state.
22    pub fn update<T: AsRef<[u8]>>(&mut self, data: T) {
23        self.0.update(data)
24    }
25
26    /// Finalize hashing, consuming the Hasher instance and returning the
27    /// resultant hash or `Digest`.
28    pub fn finalize(self) -> Digest {
29        Digest::new(self.0.finalize().into())
30    }
31
32    /// Convenience function for creating a new Hasher instance, hashing the
33    /// provided data, and returning the resultant `Digest`
34    pub fn digest<T: AsRef<[u8]>>(data: T) -> Digest {
35        let mut hasher = Self::new();
36        hasher.update(data);
37        hasher.finalize()
38    }
39}
40
41impl std::io::Write for Hasher {
42    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
43        self.0.write(buf)
44    }
45
46    fn flush(&mut self) -> std::io::Result<()> {
47        self.0.flush()
48    }
49}
50
51/// Hash the bytes written by `write` and turn the digest into an `Address`.
52fn derive_address_from(write: impl FnOnce(&mut Hasher)) -> Address {
53    let mut hasher = Hasher::new();
54    write(&mut hasher);
55    Address::new(hasher.finalize().into_inner())
56}
57
58impl crate::Ed25519PublicKey {
59    /// Derive an `Address` from this Public Key
60    ///
61    /// An `Address` can be derived from an `Ed25519PublicKey` by hashing the
62    /// bytes of the public key with no prefix flag.
63    ///
64    /// `hash(32-byte ed25519 public key)`
65    ///
66    /// ```
67    /// use iota_sdk_types::{Address, Ed25519PublicKey, hash::Hasher};
68    ///
69    /// let public_key_bytes = [0; 32];
70    /// let mut hasher = Hasher::new();
71    /// hasher.update(public_key_bytes);
72    /// let address = Address::new(hasher.finalize().into_inner());
73    /// println!("Address: {}", address);
74    ///
75    /// let public_key = Ed25519PublicKey::new(public_key_bytes);
76    /// assert_eq!(address, public_key.derive_address());
77    /// ```
78    pub fn derive_address(&self) -> Address {
79        derive_address_from(|hasher| self.write_into_hasher(hasher))
80    }
81
82    fn write_into_hasher(&self, hasher: &mut Hasher) {
83        hasher.update(self.inner());
84    }
85}
86
87impl From<crate::Ed25519PublicKey> for Address {
88    fn from(public_key: crate::Ed25519PublicKey) -> Self {
89        public_key.derive_address()
90    }
91}
92
93impl From<&crate::Ed25519PublicKey> for Address {
94    fn from(public_key: &crate::Ed25519PublicKey) -> Self {
95        public_key.derive_address()
96    }
97}
98
99impl crate::Secp256k1PublicKey {
100    /// Derive an `Address` from this Public Key
101    ///
102    /// An `Address` can be derived from a `Secp256k1PublicKey` by hashing the
103    /// bytes of the public key prefixed with the Secp256k1
104    /// `SignatureScheme` flag (`0x01`).
105    ///
106    /// `hash( 0x01 || 33-byte secp256k1 public key)`
107    ///
108    /// ```
109    /// use iota_sdk_types::{Address, Secp256k1PublicKey, hash::Hasher};
110    ///
111    /// let public_key_bytes = [0; 33];
112    /// let mut hasher = Hasher::new();
113    /// hasher.update([0x01]); // The SignatureScheme flag for Secp256k1 is `1`
114    /// hasher.update(public_key_bytes);
115    /// let address = Address::new(hasher.finalize().into_inner());
116    /// println!("Address: {}", address);
117    ///
118    /// let public_key = Secp256k1PublicKey::new(public_key_bytes);
119    /// assert_eq!(address, public_key.derive_address());
120    /// ```
121    pub fn derive_address(&self) -> Address {
122        derive_address_from(|hasher| self.write_into_hasher(hasher))
123    }
124
125    fn write_into_hasher(&self, hasher: &mut Hasher) {
126        hasher.update([self.scheme().to_u8()]);
127        hasher.update(self.inner());
128    }
129}
130
131impl From<crate::Secp256k1PublicKey> for Address {
132    fn from(public_key: crate::Secp256k1PublicKey) -> Self {
133        public_key.derive_address()
134    }
135}
136
137impl From<&crate::Secp256k1PublicKey> for Address {
138    fn from(public_key: &crate::Secp256k1PublicKey) -> Self {
139        public_key.derive_address()
140    }
141}
142
143impl crate::Secp256r1PublicKey {
144    /// Derive an `Address` from this Public Key
145    ///
146    /// An `Address` can be derived from a `Secp256r1PublicKey` by hashing the
147    /// bytes of the public key prefixed with the Secp256r1
148    /// `SignatureScheme` flag (`0x02`).
149    ///
150    /// `hash( 0x02 || 33-byte secp256r1 public key)`
151    ///
152    /// ```
153    /// use iota_sdk_types::{Address, Secp256r1PublicKey, hash::Hasher};
154    ///
155    /// let public_key_bytes = [0; 33];
156    /// let mut hasher = Hasher::new();
157    /// hasher.update([0x02]); // The SignatureScheme flag for Secp256r1 is `2`
158    /// hasher.update(public_key_bytes);
159    /// let address = Address::new(hasher.finalize().into_inner());
160    /// println!("Address: {}", address);
161    ///
162    /// let public_key = Secp256r1PublicKey::new(public_key_bytes);
163    /// assert_eq!(address, public_key.derive_address());
164    /// ```
165    pub fn derive_address(&self) -> Address {
166        derive_address_from(|hasher| self.write_into_hasher(hasher))
167    }
168
169    fn write_into_hasher(&self, hasher: &mut Hasher) {
170        hasher.update([self.scheme().to_u8()]);
171        hasher.update(self.inner());
172    }
173}
174
175impl From<crate::Secp256r1PublicKey> for Address {
176    fn from(public_key: crate::Secp256r1PublicKey) -> Self {
177        public_key.derive_address()
178    }
179}
180
181impl From<&crate::Secp256r1PublicKey> for Address {
182    fn from(public_key: &crate::Secp256r1PublicKey) -> Self {
183        public_key.derive_address()
184    }
185}
186
187impl crate::PasskeyPublicKey {
188    /// Derive an `Address` from this Passkey Public Key
189    ///
190    /// An `Address` can be derived from a `PasskeyPublicKey` by hashing the
191    /// bytes of the `Secp256r1PublicKey` that corresponds to this passkey
192    /// prefixed with the Passkey `SignatureScheme` flag (`0x06`).
193    ///
194    /// `hash( 0x06 || 33-byte secp256r1 public key)`
195    pub fn derive_address(&self) -> Address {
196        derive_address_from(|hasher| self.write_into_hasher(hasher))
197    }
198
199    fn write_into_hasher(&self, hasher: &mut Hasher) {
200        hasher.update([self.scheme().to_u8()]);
201        hasher.update(self.inner().inner());
202    }
203}
204
205impl From<crate::PasskeyPublicKey> for Address {
206    fn from(public_key: crate::PasskeyPublicKey) -> Self {
207        public_key.derive_address()
208    }
209}
210
211impl From<&crate::PasskeyPublicKey> for Address {
212    fn from(public_key: &crate::PasskeyPublicKey) -> Self {
213        public_key.derive_address()
214    }
215}
216
217impl crate::PublicKey {
218    /// Derive an `Address` from this Public Key
219    ///
220    /// See the `derive_address` documentation of the concrete key types for
221    /// the scheme-specific derivation.
222    pub fn derive_address(&self) -> Address {
223        derive_address_from(|hasher| self.write_into_hasher(hasher))
224    }
225
226    fn write_into_hasher(&self, hasher: &mut Hasher) {
227        match self {
228            Self::Ed25519(pk) => pk.write_into_hasher(hasher),
229            Self::Secp256k1(pk) => pk.write_into_hasher(hasher),
230            Self::Secp256r1(pk) => pk.write_into_hasher(hasher),
231            Self::Passkey(pk) => pk.write_into_hasher(hasher),
232        }
233    }
234}
235
236impl From<crate::PublicKey> for Address {
237    fn from(public_key: crate::PublicKey) -> Self {
238        public_key.derive_address()
239    }
240}
241
242impl From<&crate::PublicKey> for Address {
243    fn from(public_key: &crate::PublicKey) -> Self {
244        public_key.derive_address()
245    }
246}
247
248impl crate::MultisigCommittee {
249    /// Derive an `Address` from this MultisigCommittee.
250    ///
251    /// A MultiSig address is defined as the 32-byte Blake2b hash of serializing
252    /// the `SignatureScheme` flag (0x03), the threshold (in little endian), and
253    /// the concatenation of all n flags, public keys and their weights, where
254    /// `flag_i?` is the member's `SignatureScheme` flag — omitted for Ed25519
255    /// keys, matching their plain address derivation.
256    ///
257    /// `hash(0x03 || threshold || flag_1? || pk_1 || weight_1
258    /// || ... || flag_n? || pk_n || weight_n)`.
259    pub fn derive_address(&self) -> Address {
260        derive_address_from(|hasher| {
261            hasher.update([self.scheme().to_u8()]);
262            hasher.update(self.threshold().to_le_bytes());
263
264            for member in self.members() {
265                member.public_key().write_into_hasher(hasher);
266                hasher.update(member.weight().to_le_bytes());
267            }
268        })
269    }
270}
271
272impl From<crate::MultisigCommittee> for Address {
273    fn from(committee: crate::MultisigCommittee) -> Self {
274        committee.derive_address()
275    }
276}
277
278impl From<&crate::MultisigCommittee> for Address {
279    fn from(committee: &crate::MultisigCommittee) -> Self {
280        committee.derive_address()
281    }
282}
283
284impl crate::UserSignature {
285    /// Derive the `Address` of the signer that this signature authenticates.
286    pub fn derive_address(&self) -> Address {
287        match self {
288            Self::Simple(simple) => simple.to_public_key().derive_address(),
289            Self::Multisig(multisig) => multisig.committee().derive_address(),
290            Self::PasskeyAuthenticator(passkey) => passkey.public_key().derive_address(),
291            Self::MoveAuthenticator(move_authenticator) => move_authenticator.address(),
292        }
293    }
294}
295
296impl From<crate::UserSignature> for Address {
297    fn from(signature: crate::UserSignature) -> Self {
298        signature.derive_address()
299    }
300}
301
302impl From<&crate::UserSignature> for Address {
303    fn from(signature: &crate::UserSignature) -> Self {
304        signature.derive_address()
305    }
306}
307
308/// Error returned when no signature in a
309/// [`SignedTransaction`](crate::SignedTransaction) commits to an expected
310/// signer address.
311#[cfg(feature = "serde")]
312#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
313#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
314#[error("no signature found for address {address}")]
315pub struct MissingSignatureError {
316    /// The address no signature commits to.
317    pub address: Address,
318}
319
320/// Borrowed mirror of [`Transaction`](crate::Transaction) that serializes
321/// identically, allowing digests of a [`TransactionV1`](crate::TransactionV1)
322/// to be computed without cloning it into the owned enum.
323#[cfg(feature = "serde")]
324#[derive(serde::Serialize)]
325enum TransactionRef<'a> {
326    V1(&'a crate::TransactionV1),
327}
328
329#[cfg(feature = "serde")]
330#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
331mod type_digest {
332    use super::Hasher;
333    use crate::{
334        CheckpointContentsDigest, CheckpointDigest, Digest, ObjectDigest, SenderSignedDataDigest,
335        TransactionDigest, TransactionEffectsDigest, TransactionEventsDigest,
336    };
337
338    impl crate::Object {
339        /// Calculate the digest of this `Object`
340        ///
341        /// This is done by hashing the BCS bytes of this `Object` prefixed
342        /// with a salt.
343        pub fn digest(&self) -> ObjectDigest {
344            const SALT: &str = "Object::";
345            type_digest(SALT, self).into()
346        }
347    }
348
349    impl crate::CheckpointSummary {
350        pub fn digest(&self) -> CheckpointDigest {
351            const SALT: &str = "CheckpointSummary::";
352            type_digest(SALT, self).into()
353        }
354    }
355
356    impl crate::CheckpointContents {
357        pub fn digest(&self) -> CheckpointContentsDigest {
358            const SALT: &str = "CheckpointContents::";
359            type_digest(SALT, self).into()
360        }
361    }
362
363    impl crate::Transaction {
364        pub fn digest(&self) -> TransactionDigest {
365            const SALT: &str = "TransactionData::";
366            type_digest(SALT, self).into()
367        }
368    }
369
370    impl crate::TransactionV1 {
371        pub fn digest(&self) -> TransactionDigest {
372            const SALT: &str = "TransactionData::";
373            type_digest(SALT, &super::TransactionRef::V1(self)).into()
374        }
375    }
376
377    impl crate::SenderSignedTransaction {
378        /// Calculate the digest of the full message, committing to the signing
379        /// intent, the transaction, and all signatures.
380        ///
381        /// Unlike the other type digests, this hashes the BCS bytes directly,
382        /// without a salt prefix.
383        pub fn full_message_digest(&self) -> SenderSignedDataDigest {
384            let mut hasher = Hasher::new();
385            bcs::serialize_into(&mut hasher, self).expect("bcs serialization failed");
386            hasher.finalize().into()
387        }
388    }
389
390    impl crate::TransactionEffects {
391        pub fn digest(&self) -> TransactionEffectsDigest {
392            const SALT: &str = "TransactionEffects::";
393            type_digest(SALT, self).into()
394        }
395    }
396
397    impl crate::TransactionEvents {
398        pub fn digest(&self) -> TransactionEventsDigest {
399            const SALT: &str = "TransactionEvents::";
400            type_digest(SALT, self).into()
401        }
402    }
403
404    impl crate::MoveAuthenticator {
405        pub fn digest(&self) -> Digest {
406            const SALT: &str = "MoveAuthenticator::";
407            type_digest(SALT, self)
408        }
409    }
410
411    impl crate::UserSignature {
412        /// Calculate the auth digest for this signature.
413        ///
414        /// For [`MoveAuthenticator`](crate::MoveAuthenticator) signatures this
415        /// equals
416        /// [`MoveAuthenticator::digest()`](crate::MoveAuthenticator::digest).
417        /// For all other signature types it is the Blake2b256 of the
418        /// serialized (flag-prefixed) signature bytes.
419        pub fn auth_digest(&self) -> Digest {
420            match self {
421                Self::MoveAuthenticator(authenticator) => authenticator.digest(),
422                Self::Simple(_) | Self::Multisig(_) | Self::PasskeyAuthenticator(_) => {
423                    Hasher::digest(self.to_bytes())
424                }
425            }
426        }
427    }
428
429    impl crate::SignedTransaction {
430        /// Computes the auth digest for the sender and, if sponsored, for the
431        /// sponsor. See
432        /// [`UserSignature::auth_digest`](crate::UserSignature::auth_digest)
433        /// for the per-signature logic.
434        ///
435        /// Returns an error if no signature commits to the sender or sponsor
436        /// address.
437        pub fn compute_auth_digests(
438            &self,
439        ) -> Result<(Digest, Option<Digest>), super::MissingSignatureError> {
440            let crate::Transaction::V1(transaction) = &self.transaction;
441
442            let digest_for_address = |address| {
443                self.signatures
444                    .iter()
445                    .find(|signature| signature.derive_address() == address)
446                    .map(crate::UserSignature::auth_digest)
447                    .ok_or(super::MissingSignatureError { address })
448            };
449
450            let sender_auth_digest = digest_for_address(transaction.sender)?;
451            let gas_owner = transaction.gas_payment.owner;
452            let sponsor_auth_digest = if gas_owner != transaction.sender {
453                Some(digest_for_address(gas_owner)?)
454            } else {
455                None
456            };
457
458            Ok((sender_auth_digest, sponsor_auth_digest))
459        }
460    }
461
462    fn type_digest<T: serde::Serialize>(salt: &str, ty: &T) -> Digest {
463        let mut hasher = Hasher::new();
464        hasher.update(salt);
465        bcs::serialize_into(&mut hasher, ty).expect("bcs serialization failed");
466        hasher.finalize()
467    }
468}
469
470#[cfg(feature = "serde")]
471#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
472mod signing_message {
473    use crate::{
474        Intent, IntentMessage, IntentScope, PersonalMessage, SigningDigest, Transaction,
475        TransactionV1, hash::Hasher,
476    };
477
478    impl Transaction {
479        pub fn signing_digest(&self) -> SigningDigest {
480            self.intent_message().signing_digest()
481        }
482
483        pub fn signing_digest_hex(&self) -> String {
484            hex::encode(self.signing_digest())
485        }
486    }
487
488    impl TransactionV1 {
489        pub fn signing_digest(&self) -> SigningDigest {
490            IntentMessage::new(Intent::iota_transaction(), super::TransactionRef::V1(self))
491                .signing_digest()
492        }
493
494        pub fn signing_digest_hex(&self) -> String {
495            hex::encode(self.signing_digest())
496        }
497    }
498
499    impl PersonalMessage<'_> {
500        pub fn signing_digest(&self) -> SigningDigest {
501            IntentMessage::new(Intent::personal_message(), &self.0).signing_digest()
502        }
503
504        pub fn signing_digest_hex(&self) -> String {
505            hex::encode(self.signing_digest())
506        }
507    }
508
509    impl crate::CheckpointSummary {
510        pub fn signing_message(&self) -> Vec<u8> {
511            let mut message = Vec::new();
512            message.extend(Intent::iota_app(IntentScope::CheckpointSummary).to_bytes());
513            bcs::serialize_into(&mut message, self).expect("bcs serialization failed");
514            bcs::serialize_into(&mut message, &self.epoch).expect("bcs serialization failed");
515            message
516        }
517
518        pub fn signing_message_hex(&self) -> String {
519            hex::encode(self.signing_message())
520        }
521    }
522
523    impl<T> IntentMessage<T>
524    where
525        T: serde::Serialize,
526    {
527        pub fn signing_digest(&self) -> SigningDigest {
528            let mut hasher = Hasher::new();
529            bcs::serialize_into(&mut hasher, self).expect("bcs serialization failed");
530            hasher.finalize().into()
531        }
532    }
533}
534
535/// A 1-byte domain separator for hashing Object ID in IOTA. It starts from 0xf0
536/// to ensure no hashing collision for any ObjectId vs Address which is
537/// derived as the hash of `flag || pubkey`.
538#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
539#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
540#[repr(u8)]
541enum HashingIntent {
542    #[cfg(feature = "serde")]
543    ChildObjectId = 0xf0,
544    RegularObjectId = 0xf1,
545}
546
547impl crate::ObjectId {
548    /// Create an ObjectId from a transaction digest and `count`.
549    ///
550    /// `count` is the number of objects that have been created during a
551    /// transaction.
552    pub fn derive_id(digest: crate::TransactionDigest, 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 ObjectId of a derived object (`0x2::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        // Wrap the key type into `DerivedObjectKey<K>` to preserve on-chain
590        // namespacing
591        let wrapper_type_tag = crate::TypeTag::Struct(Box::new(crate::StructTag::new(
592            crate::Address::FRAMEWORK,
593            crate::Identifier::DERIVED_OBJECT_MODULE,
594            crate::Identifier::DERIVED_OBJECT_KEY,
595            vec![key_type_tag.clone()],
596        )));
597
598        self.derive_dynamic_child_id(&wrapper_type_tag, key_bytes)
599    }
600}
601
602#[cfg(all(test, feature = "proptest"))]
603mod tests {
604    use test_strategy::proptest;
605
606    use super::HashingIntent;
607    use crate::SignatureScheme;
608
609    impl HashingIntent {
610        fn from_byte(byte: u8) -> Result<Self, u8> {
611            match byte {
612                0xf0 => Ok(Self::ChildObjectId),
613                0xf1 => Ok(Self::RegularObjectId),
614                invalid => Err(invalid),
615            }
616        }
617    }
618
619    #[proptest]
620    fn hashing_intent_does_not_overlap_with_signature_scheme(intent: HashingIntent) {
621        SignatureScheme::from_byte(intent as u8).unwrap_err();
622    }
623
624    #[proptest]
625    fn signature_scheme_does_not_overlap_with_hashing_intent(scheme: SignatureScheme) {
626        HashingIntent::from_byte(scheme.to_u8()).unwrap_err();
627    }
628
629    #[proptest]
630    fn roundtrip_hashing_intent(intent: HashingIntent) {
631        assert_eq!(Ok(intent), HashingIntent::from_byte(intent as u8));
632    }
633
634    // Guards that `TransactionRef` stays serialization-identical to
635    // `Transaction`.
636    #[cfg(feature = "serde")]
637    #[proptest]
638    fn transaction_v1_digests_match_transaction(transaction: crate::TransactionV1) {
639        let digest = transaction.digest();
640        let signing_digest = transaction.signing_digest();
641        let transaction = crate::Transaction::V1(transaction);
642        assert_eq!(digest, transaction.digest());
643        assert_eq!(signing_digest, transaction.signing_digest());
644    }
645}
646
647#[cfg(all(test, feature = "serde"))]
648mod serde_tests {
649    use std::str::FromStr;
650
651    use base64ct::Encoding;
652
653    use crate::{Address, Identifier, ObjectId, StructTag, TypeTag, UserSignature};
654
655    // Guards the address derivation from serialized signatures: every
656    // UserSignature kind, given as base64, must keep deriving the same address.
657    #[test]
658    fn test_address_from_user_signature() {
659        let fixtures = [
660            // Ed25519
661            (
662                "AO/2qtqkYPqq3UzI7dVLmqt7dy2B5Ta2Hv7F1ssYO9auPyrcRGpawALnzyNyPBT/v/PIxSbNTskTs+ts6kGtkQYNfas1jI2tqk76AEmnWwdDZVWxCjaCGbtoD3BXE0nXdQ==",
663                "0xebb23f93d022ac213e99ac7d85b7f7e1e4a18f045b379755565cffa08804c9a1",
664            ),
665            // Secp256k1
666            (
667                "ASWDBpw4ETzLiQlwS0kDKJA9PK47V8fp4e9S+7bpaJXfd4HfZ5oLSXTyezaHTRfcryQn3mdshteXwrEvj/ZAmiUCDhfNWTnkaxlmQZaM11mRC6JXfif6c/3jh225vsW86ys=",
668                "0xd9607cd03428c904949572b51471e7a9f60019aeb9a3d7ee5e72921cab8e8be7",
669            ),
670            // Secp256r1
671            (
672                "Ai1Hdv4ZtEslPN424BGG5+6BhzGrp4d4ykoyiQhG2hDzbGDdCPgXVLLv26sfFP77nhJi78OAWfD+ytdQRyYPpKcDR/uvI/A4q8TDCKJxEXoqTP+u3bxf+Bx1F7xsdKfttDA=",
673                "0x600b1081644fe46f76da3bdc19f8743b9f04458516364374c7d82959e790c19e",
674            ),
675            // MultiSig (2-of-3: Ed25519 + Secp256k1, over the pubkeys above)
676            (
677                "AwIArJTVgdzrD8V+em1zREsmT0jD/fduXgh/zo+Z+lZTYoBLZpaAw9PNO4YNDYBTK9vT646klVBV4ntBmFcYzdDVDQGia/pl6A/nh/uixuM1hc84fokvTk37j/DNq3/OtckvsV1Mg84ygJ7UXsuohtKjHc+zfuu2uwcjh2lOPVBgZotQAwADAA19qzWMja2qTvoASadbB0NlVbEKNoIZu2gPcFcTSdd1AQECDhfNWTnkaxlmQZaM11mRC6JXfif6c/3jh225vsW86ysBAgNH+68j8DirxMMIonEReipM/67dvF/4HHUXvGx0p+20MAECAA==",
678                "0x34b66b6d090baea4effa1d1bf22e1adff466c3fb9425ded9c5bbb605865b2560",
679            ),
680            // PasskeyAuthenticator (Secp256r1-backed)
681            (
682                "BgByeyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHBzOi8vdGVzdC5pb3RhLm9yZyJ9YgJsW7qQ7IeOJMhUEgb7PFfoHEFCbk7qLZ27Nhqxpq8OLCapbkIG0nOO1iQgY7KjGPykr/gcDF+FtKxxcebcDOPXA0f7ryPwOKvEwwiicRF6Kkz/rt28X/gcdRe8bHSn7bQw",
683                "0x79214f7090abbaf3d14fdbe357ea794650f41783ea64ece65314682144f658a7",
684            ),
685            // MoveAuthenticator
686            (
687                "BwAAAAEB7mKDoFTKsmYgpTHRjEnqNWAjnP2LqDo5O7qn1+7SCCoBAAAAAAAAAAA=",
688                "0xee6283a054cab26620a531d18c49ea3560239cfd8ba83a393bbaa7d7eed2082a",
689            ),
690        ];
691
692        for (b64, expected) in fixtures {
693            let sig = UserSignature::from_base64(b64).unwrap();
694            assert_eq!(sig.derive_address().to_string(), expected);
695        }
696
697        // zkLogin (flag 0x05) is deprecated: serialized signatures are rejected
698        // at deserialization.
699        let zklogin_b64 = base64ct::Base64::encode_string(&[0x05u8, 0, 0, 0]);
700        assert!(UserSignature::from_base64(&zklogin_b64).is_err());
701    }
702
703    // Snapshot tests that match the on-chain `derive_address` logic.
704    // These snapshots can also be found in the `derived_object_tests.move` unit
705    // tests.
706    #[test]
707    fn test_derive_object_id_snapshot() {
708        let key_bytes = bcs::to_bytes("foo".as_bytes()).unwrap();
709        let key_type_tag = TypeTag::Vector(Box::new(TypeTag::U8));
710
711        let id = ObjectId::from_str("0x2")
712            .unwrap()
713            .derive_object_id(&key_type_tag, &key_bytes);
714
715        assert_eq!(
716            id,
717            ObjectId::from_str(
718                "0xa2b411aa9588c398d8e3bc97dddbdd430b5ded7f81545d05e33916c3ca0f30c3"
719            )
720            .unwrap()
721        );
722    }
723
724    #[test]
725    fn test_derive_object_id_with_struct_key_snapshot() {
726        #[derive(serde::Serialize)]
727        struct DemoStruct {
728            value: u64,
729        }
730
731        let key_bytes = bcs::to_bytes(&DemoStruct { value: 1 }).unwrap();
732        let key_type_tag = TypeTag::Struct(Box::new(StructTag::new(
733            Address::FRAMEWORK,
734            Identifier::from_static("derived_object_tests"),
735            Identifier::from_static("DemoStruct"),
736            vec![],
737        )));
738
739        let id = ObjectId::from_str("0x2")
740            .unwrap()
741            .derive_object_id(&key_type_tag, &key_bytes);
742
743        assert_eq!(
744            id,
745            ObjectId::from_str(
746                "0x20c58d8790a5d2214c159c23f18a5fdc347211e511186353e785ad543abcea6b"
747            )
748            .unwrap()
749        );
750    }
751
752    #[test]
753    fn test_derive_object_id_with_generic_struct_key_snapshot() {
754        #[derive(serde::Serialize)]
755        struct GenericStruct<T> {
756            value: T,
757        }
758
759        let key_bytes = bcs::to_bytes(&GenericStruct::<u64> { value: 1 }).unwrap();
760        let key_type_tag = TypeTag::Struct(Box::new(StructTag::new(
761            Address::FRAMEWORK,
762            Identifier::from_static("derived_object_tests"),
763            Identifier::from_static("GenericStruct"),
764            vec![TypeTag::U64],
765        )));
766
767        let id = ObjectId::from_str("0x2")
768            .unwrap()
769            .derive_object_id(&key_type_tag, &key_bytes);
770
771        assert_eq!(
772            id,
773            ObjectId::from_str(
774                "0xb497b8dcf1e297ae5fa69c040e4a08ef8240d5373bbc9d6b686ffbd7dfe04cbe"
775            )
776            .unwrap()
777        );
778    }
779}