Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

PackageMetadata is an immutable on-chain object that provides trusted metadata about Move packages during execution. Because PackageMetadata objects 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:

  1. Trusting user input:
    • accepting claims about packages without verification
    • drawback: user input can be malicious
  2. Hardcoding knowledge:
    • embedding package-specific logic in modules
    • drawback: hardcoding doesn’t scale
  3. Off-chain verification:
    • checking properties before transaction submission
    • drawback: user input can be malicious

PackageMetadata solves this by providing protocol-attested package introspection. Because only the protocol can create PackageMetadata objects (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 PackageMetadata could be:

  1. Account Abstraction (planned) (see IIP-0009): Move code reads PackageMetadata to verify that a function is a valid authenticator and to obtain the account type it authenticates. This enables the account module to create AuthenticatorInfoV1 instances that reference verified authenticator functions.
  2. View Functions (planned) (see IIP-0005): Modules and clients can discover which functions are safe to call without state changes.
  3. Capability Verification: Modules can verify package capabilities before granting access.
  4. 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:

  1. Protocol-only creation: PackageMetadata objects 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 PackageMetadata content is derived from verified bytecode
    • Users cannot forge or tamper with metadata
    • Move code can trust metadata without additional verification
  2. Immutability: PackageMetadata objects are frozen immediately upon creation.
  3. Conditional Creation: PackageMetadata is 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 no PackageMetadata object.
  4. Deterministic PackageMetadata id derivation: Given any package id, the corresponding PackageMetadata object 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 RuntimeModuleMetadata structure and embedded directly into the module’s bytecode. Once all attributes for a module are collected, the RuntimeModuleMetadataV1 is wrapped in a RuntimeModuleMetadataWrapper (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_KEY is 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:

  1. Single Entry: A module may have at most one metadata entry with the IOTA_METADATA_KEY
  2. Valid Structure: The bytes must deserialize to a valid RuntimeModuleMetadataWrapper
  3. 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 RuntimeModuleMetadata from each module’s bytecode using this the IOTA_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 calling verify_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_KEY slot 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 PackageMetadataV1 object. For each module containing verified attributes, it creates a ModuleMetadataV1 entry. For authenticator attributes specifically, it extracts the first parameter’s type from the verified function signature, this becomes the account_type field, representing which object type this authenticator can authenticate.

Finally, the protocol creates the PackageMetadataV1 object 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 derivation
  • HashingIntentScope::ChildObjectId - The flag used to avoid hash collisions. Hardcoded value of 240 (or 0xF0).
  • || - Concatenation

This 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 PackageMetadata object by borrowing it as an immutable reference. For example, when the Account Abstraction framework needs to create an AuthenticatorFunctionRefV1 for an account, it reads the relevant PackageMetadataV1 object, looks up the authenticator by module and function name, and extracts the account_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 PackageMetadata can 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.

PackageMetadata is 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 new PackageMetadata object dedicated to the new version is created. Moreover, PackageMetadata objects 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

  1. Existing Packages: For packages published before PackageMetadata introduction no metadata object exists and these packages continue to function normally.
  2. Package Upgrades: Each upgrade creates a new PackageMetadata for 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.
  3. Adding New Attributes and Fields to the Model: New attributes can be added without breaking existing code adding a variant to IotaAttribute enum or a field to ModuleMetadata and increase the PackageMetadata version, i.e., PackageMetadataV2, V3, etc. Existing metadata continues to work.

Test Cases

  1. Abstracted IOTA Accounts Authenticator Functions.
  2. 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

  1. Metadata for Non-Attributed Packages: Should minimal metadata be created for all packages (e.g., just IDs and version)?
  2. Cross-Package Queries: Should Move code be able to query metadata for arbitrary packages, or only those passed as arguments?
  3. Metadata Expiration: Should old package version metadata eventually be prunable?

Future Work

Planned Attributes

AttributePurposeMetadata Fields
#[authenticator]Account authenticationfunction_name, account_type
#[view]Read-only functionsfunction_name, return_type

Tooling

  • CLI: iota package metadata <package-id>
  • GraphQL: Package metadata queries
  • Explorer: Metadata visualization

Copyright and related rights waived via CC0.