iip: 10 title: Package Metadata description: Immutable on-chain object that provides trusted metadata about Move packages during execution. author: Mirko Zichichi (@miker83z), Valerii Reutov (@valeriyr) discussions-to: https://github.com/iotaledger/IIPs/discussions/36 status: Draft type: Standards Track layer: Core created: 2026-02-17 requires: None Abstract
PackageMetadatais an immutable on-chain object that provides trusted metadata about Move packages during execution. BecausePackageMetadataobjects are created exclusively by the protocol during publish and upgrade operations, Move code can read this metadata with full confidence in its authenticity. This enables on-chain verification of package properties without relying on user-provided claims. This mechanism allows Move modules to introspect package capabilities, verify function signatures, and make decisions based on protocol-attested information.Motivation
Move execution might require knowledge about external packages or the same package being used: What functions does a package expose? What capabilities does it claim? Is a given function a valid authenticator?
Traditionally, answering these questions required either:
- Trusting user input:
- accepting claims about packages without verification
- drawback: user input can be malicious
- Hardcoding knowledge:
- embedding package-specific logic in modules
- drawback: hardcoding doesn’t scale
- Off-chain verification:
- checking properties before transaction submission
- drawback: user input can be malicious
PackageMetadatasolves this by providing protocol-attested package introspection. Because only the protocol can createPackageMetadataobjects (during publish/upgrade), and because these objects are immutable, Move code can trust their contents completely. This enables:
- On-chain capability discovery: Modules can query what a package provides.
- Dynamic integration: Modules can work with packages they were not compiled against (at the metadata level).
- Protocol-enforced properties: Metadata reflects verified attributes; no need to trust user claims about packages.
Possible use cases exploiting
PackageMetadatacould be:
- Account Abstraction (planned) (see IIP-0009): Move code reads
PackageMetadatato verify that a function is a valid authenticator and to obtain the account type it authenticates. This enables theaccountmodule to createAuthenticatorInfoV1instances that reference verified authenticator functions.- View Functions (planned) (see IIP-0005): Modules and clients can discover which functions are safe to call without state changes.
- Capability Verification: Modules can verify package capabilities before granting access.
- Function modifiers: Modules can parse functions of any package to check whether they are entry, private, etc.
Specification
In this section, we present the technical specification for implementing an Package Metadata model version 1 within the IOTA protocol. The specification begins by outlining a set of functional requirements the model must satisfy, followed by a high-level overview of the proposed architectural approach. Finally, the main set of Move type interfaces will be provided as standard for the first version of this model.
Requirements
The proposed Package Metadata model must adhere to the following constraints:
- Protocol-only creation:
PackageMetadataobjects can only be created by the protocol during publish or upgrade execution. There is no public constructor or creation function exposed to Move code. This guarantees that:
- All
PackageMetadatacontent is derived from verified bytecode- Users cannot forge or tamper with metadata
- Move code can trust metadata without additional verification
- Immutability:
PackageMetadataobjects are frozen immediately upon creation.- Conditional Creation:
PackageMetadatais created only when meaningful metadata exists, e.g., at least one recognized attribute must be present in a module of the package. Packages without attributes have noPackageMetadataobject.- Deterministic
PackageMetadataid derivation: Given any package id, the correspondingPackageMetadataobject id can be computed using the derived object mechanism (same as dynamic fields id derivation). See https://docs.sui.io/guides/developer/objects/derived-objects. Move code can compute this derivation on-chain.High-Level Overview
To support
PackageMetadata, we propose to modify part of the Move compilation, part of the publish/upgrade execution and the addition of a new module to the iota-framework.In the following, we are going to use the usage of Package Metadata within the IOTA Account Abstraction model (see IIP-0009), because that is a concrete use of the standard.
┌─────────────────────────────────────────────────────────────────┐ │ 1. COMPILATION (Developer Machine) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ #[authenticator] ┌──────────────────────┐ │ │ public fun authenticate(..) │ RuntimeModuleMetadata│ │ │ │ │ embedded in bytecode │ │ │ └─────────────────────▶│ │ │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 2. PUBLISH/UPGRADE (Protocol Execution) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ │ Extract metadata │───▶│ Verify each attribute │ │ │ │ from bytecode │ │ (authenticator sig check) │ │ │ └─────────────────────┘ └────────────────────────────┘ │ │ │ │ │ │ │ ┌────────────────────┘ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ PROTOCOL creates PackageMetadataV1 │ │ │ │ - Populates from verified attributes │ │ │ │ - Derives object ID from package ID │ │ │ │ - Freezes object (immutable) │ │ │ └─────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 3. RUNTIME (Move VM Execution) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ // Any Move code can read and trust this metadata │ │ │ │ public fun create_authenticator_info( │ │ metadata: &PackageMetadataV1, // Protocol-created │ │ module_name: String, │ │ function_name: String, │ │ ): AuthenticatorInfoV1 { │ │ // Safe: metadata is protocol-verified │ │ let auth = metadata.get_authenticator(module_name, fn); │ │ // auth.account_type is TRUSTWORTHY │ │ AuthenticatorInfoV1 { ... } │ │ } │ └─────────────────────────────────────────────────────────────────┘Compilation and building phase
The process begins on the developer’s machine during package compilation. When the Move compiler encounters a function annotated with a recognized attribute (such as
#[authenticator]), it records this information in the function’s metadata. The compiler performs initial syntax validation, ensuring the attribute is well-formed and applied to an appropriate element, but does not verify semantic correctness (e.g., whether the function signature actually satisfies authenticator requirements).During the build phase, the collected attribute information is serialized into a
RuntimeModuleMetadatastructure and embedded directly into the module’s bytecode. Once all attributes for a module are collected, theRuntimeModuleMetadataV1is wrapped in aRuntimeModuleMetadataWrapper(which includes a version number) and serialized to BCS bytes. These bytes are then pushed into the module’s bytecode metadata vector using a dedicated key.The
IOTA_METADATA_KEYis a protocol-defined constant that acts as a reserved namespace. While the bytecode format allows arbitrary metadata entries, the protocol’s verifier enforces strict rules:
- Single Entry: A module may have at most one metadata entry with the
IOTA_METADATA_KEY- Valid Structure: The bytes must deserialize to a valid
RuntimeModuleMetadataWrapper- Verified Content: Each attribute within the metadata must pass its corresponding verifier
Finally, the metadata will travels with the bytecode through the publish transaction, ensuring the protocol has access to the original annotations.
Publish/Upgrade phase
When a publish or upgrade transaction is executed, the protocol takes over. This phase is critical because it establishes the trust boundary: everything that happens here is performed by the protocol itself, not by user code.
Verification
During publish or upgrade, the protocol extracts
RuntimeModuleMetadatafrom each module’s bytecode using this theIOTA_METADATA_KEY, deserializes it, and verifies each attribute. For every attribute found, the protocol invokes the corresponding verifier. For authenticator attributes, for instance, this means callingverify_authenticate_func_v1(), which checks that the function has the correct visibility, parameter types, and return type (see IIP-0009).If any verification fails, the entire publish or upgrade transaction fails. User code cannot write to the
IOTA_METADATA_KEYslot in a way that would bypass verification, because the verifier runs before execution completes, and any invalid metadata causes the transaction to fail.Object creation
Once all attributes are verified, the protocol constructs the
PackageMetadataV1object. For each module containing verified attributes, it creates aModuleMetadataV1entry. For authenticator attributes specifically, it extracts the first parameter’s type from the verified function signature, this becomes theaccount_typefield, representing which object type this authenticator can authenticate.Finally, the protocol creates the
PackageMetadataV1object and immediately freezes it, making it immutable. The object is stored on-chain with no owner (immutable objects have no owner), ensuring it cannot be modified or deleted.Object id derivation
During the creation, the protocol derives the metadata object’s ID deterministically from the package’s storage ID, reusing the dynamic field address derivation logic:
#![allow(unused)] fn main() { package_metadata_id = derive_object_id(package_storage_id, <0x2::package_metadata::PackageMetadataKey>, {/* dummy bool */}) = derive_dynamic_field_id(package_storage_id, <0x2::derived_object::DerivedObjectKey<0x2::package_metadata::PackageMetadataKey>>, {/* dummy bool */}) = Blake2b256( HashingIntentScope::ChildObjectId /* 0xF0 */ || package_storage_id || len({/* dummy bool */}) || {/* dummy bool */} || bcs(<0x2::derived_object::DerivedObjectKey<0x2::package_metadata::PackageMetadataKey>>) ) }Where:
package_storage_id- The object ID of the package (treated as the “parent”)<0x2::derived_object::DerivedObjectKey<0x2::package_metadata::PackageMetadataKey>>- The full type tag wrapping the key type, i.e.,<0x2::package_metadata::PackageMetadataKey>, (which can be arbitrary for the derived object mechanism) in the<0x2::derived_object::DerivedObjectKey<T>>type (which is hardcoded in this mechanism).{/* dummy bool */}- The key bytes, which in the case of PackageMetadataKey contains only a dummy bool field.bcs()- The BCS serialization of a key type.Blake2b256()- The hashing function used for the ID derivationHashingIntentScope::ChildObjectId- The flag used to avoid hash collisions. Hardcoded value of 240 (or 0xF0).||- ConcatenationThis derivation ensures that given any package ID, the corresponding metadata ID can always be computed without an on-chain lookup.
Runtime phase
After a successful publish/upgrade, any Move code can read the
PackageMetadataobject by borrowing it as an immutable reference. For example, when the Account Abstraction framework needs to create anAuthenticatorFunctionRefV1for an account, it reads the relevantPackageMetadataV1object, looks up the authenticator by module and function name, and extracts theaccount_type. This type information is trustworthy because it was extracted from verified bytecode by the protocol, i.e., not provided by user input or claimed by the package developer.public fun create_auth_function_ref_v1<Account: key>( package_metadata: &PackageMetadataV1, module_name: ascii::String, function_name: ascii::String, ): AuthenticatorFunctionRefV1<Account> { // TRUST: metadata was created by protocol, not user let authenticator_metadata = package_metadata .modules_metadata_v1( &module_name, ) .authenticator_metadata_v1(&function_name); // TRUST: account_type was extracted from VERIFIED bytecode assert!( type_name::get<Account>() == authenticator_metadata.account_type(), EAuthenticatorFunctionRefV1NotCompatibleWithAccount, ); AuthenticatorFunctionRefV1 { package: package_metadata.storage_id(), module_name, function_name, } }Move Types and Methods Specification
Main Types:
/// Key type for deriving the package metadata object address public struct PackageMetadataKey has copy, drop, store {} /// Represents the metadata of a Move package. This includes information /// such as the storage ID, runtime ID, version, and metadata for the /// functions contained within the package. public struct PackageMetadataV1 has key { id: UID, /// Storage ID of the package represented by this metadata /// The object id of the runtime package metadata object is derived from /// this value. storage_id: ID, /// Runtime ID of the package represented by this metadata. Runtime ID is /// the Storage ID of the first version of a package. runtime_id: ID, /// Version of the package represented by this metadata package_version: u64, // Handles to internal package modules modules_metadata: VecMap<ascii::String, ModuleMetadataV1>, } /// Represents metadata associated with a module in the package. /// V1 includes only the authenticator functions information. public struct ModuleMetadataV1 has copy, drop, store { authenticator_metadata: vector<AuthenticatorMetadataV1>, } /// Represents metadata for an authenticator within the package. /// It includes the name of the authenticate function and the TypeName /// of the first parameter (i.e., the account object type). public struct AuthenticatorMetadataV1 has copy, drop, store { function_name: ascii::String, account_type: TypeName, }Key Accessor Functions:
/// Return the version of the package represented by this metadata public fun package_version(metadata: &PackageMetadataV1): u64 { } /// Safely get the module metadata list of the package represented by this metadata public fun try_get_modules_metadata_v1( self: &PackageMetadataV1, module_name: &ascii::String, ): Option<ModuleMetadataV1> { } /// Borrow the module metadata list of the package represented by this metadata. /// Aborts if the module is not found. public fun modules_metadata_v1( self: &PackageMetadataV1, module_name: &ascii::String, ): &ModuleMetadataV1 { } /// Safely get the `AuthenticatorMetadataV1` associated with the specified /// `function_name` within the module metadata. public fun try_get_authenticator_metadata_v1( self: &ModuleMetadataV1, function_name: &ascii::String, ): Option<AuthenticatorMetadataV1> { } /// Borrow the `AuthenticatorMetadataV1` associated with the specified /// `function_name`. /// Aborts if the authenticator metadata is not found for that function. public fun authenticator_metadata_v1( self: &ModuleMetadataV1, function_name: &ascii::String, ): &AuthenticatorMetadataV1 { } /// Return the account type of the authenticator represented by this metadata public fun account_type(self: &AuthenticatorMetadataV1): TypeName { }Rationale
When the protocol creates metadata, it does so by extracting information from verified bytecode, not from user claims or developer assertions. This means Move code reading
PackageMetadatacan trust its contents implicitly: if the metadata says a function is a valid authenticator with a specific account type, that fact has been verified by the protocol during publish.
PackageMetadatais frozen immediately upon creation because the information it represents, i.e., a package metadata, is itself immutable. Once a package is published, its bytecode cannot change, so metadata derived from that bytecode should not change either. If a package is upgraded, then a newPackageMetadataobject dedicated to the new version is created. Moreover,PackageMetadataobjects are only created when a package contains at least one recognized attribute.Computing metadata IDs deterministically from package IDs means that any code, on-chain Move or off-chain tooling, can calculate a package metadata ID without performing a lookup. This eliminates the need to store the mapping explicitly and ensures the relationship between package and metadata is inherent rather than recorded.
Backwards Compatibility
- Existing Packages: For packages published before
PackageMetadataintroduction no metadata object exists and these packages continue to function normally.- Package Upgrades: Each upgrade creates a new
PackageMetadatafor that version, but old metadata objects remain always valid and accessible. package_version distinguishes between versions and runtime_id links all versions to the original package.- Adding New Attributes and Fields to the Model: New attributes can be added without breaking existing code adding a variant to
IotaAttributeenum or a field toModuleMetadataand increase thePackageMetadataversion, i.e.,PackageMetadataV2,V3, etc. Existing metadata continues to work.Test Cases
- Abstracted IOTA Accounts Authenticator Functions.
- Move View Functions
Reference Implementation
Main PR against the develop branch: https://github.com/iotaledger/iota/pull/9586. See IIP-0009.
Questions and Open Issues
- Metadata for Non-Attributed Packages: Should minimal metadata be created for all packages (e.g., just IDs and version)?
- Cross-Package Queries: Should Move code be able to query metadata for arbitrary packages, or only those passed as arguments?
- Metadata Expiration: Should old package version metadata eventually be prunable?
Future Work
Planned Attributes
Attribute Purpose Metadata Fields #[authenticator]Account authentication function_name, account_type #[view]Read-only functions function_name, return_type Tooling
- CLI:
iota package metadata <package-id>- GraphQL: Package metadata queries
- Explorer: Metadata visualization
Copyright
Copyright and related rights waived via CC0.