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: 13
title: Dynamic Module Metadata
description: Store package module metadata in dynamic fields so it can be extended without changing the package metadata object type.
author: Lorenzo Benetollo (@lollobene) , Mirko Zichichi (@miker83z) 
discussions-to: https://github.com/iotaledger/IIPs/discussions/47
status: Draft
type: Standards Track
layer: Core
created: 2026-06-17
requires: IIP-0010, IIP-0011

Abstract

This proposal refines the PackageMetadata model defined in IIP-0010 by moving per-module metadata out of the immutable PackageMetadataV1 object and into dynamic fields. Instead of embedding modules’ metadata into the PackageMetadataV1’s field modules_metadata: VecMap<String, ModuleMetadataV1>, a new set of dynamic fields is added to the PackageMetadataV1 object for each module, i.e., a ModuleMetadata object. Each ModuleMetadata is itself an extensible key-value store backed by dynamic fields, where every key holds a distinct kind of module metadata entry.

This makes the set of recorded module metadata kinds open-ended: new kinds can be added by writing a new dynamic field, without changing the PackageMetadata object type or introducing a new top-level struct version. The first new kind enabled by this layout is view function metadata — the list of #[view] functions (IIP-0011) per module — recorded alongside the existing authenticator metadata.

The change is gated behind a protocol feature flag and is backwards compatible: packages published under the previous layout keep working, and the legacy accessors continue to function.

Motivation

IIP-0010 stores all module metadata inline in the frozen PackageMetadataV1 object:

public struct PackageMetadataV1 has key {
    id: UID,
    storage_id: ID,
    runtime_id: ID,
    package_version: u64,
    modules_metadata: VecMap<ascii::String, ModuleMetadataV1>,
}

public struct ModuleMetadataV1 has copy, drop, store {
    authenticator_metadata: vector<AuthenticatorMetadataV1>,
}

This shape couples the content of module metadata to the type of the metadata object. IIP-0010 already anticipated that new metadata kinds (e.g. view functions) would be added, and proposed handling growth by bumping the struct version: PackageMetadataV2, V3, and so on (see IIP-0010 — Backwards Compatibility).

Versioning the struct type for every new metadata kind has two drawbacks:

  1. Type churn: every new kind of module metadata forces a new ModuleMetadata/PackageMetadata struct version, and every reader must learn the new type. A struct field is also strongly typed and ordered, which is a poor fit for an open, growing set of optional metadata kinds.
  2. No partial reads: an inline VecMap is read as a whole. As more kinds are added, readers and tooling that only care about one kind (say, view functions) still materialize the entire module metadata.

IIP-0011 introduces #[view] functions and states that view functions are exposed through PackageMetadata, “similarly to how authenticator functions are currently exposed”, so that clients and tooling can discover them without static analysis. Adding view function metadata is therefore the first concrete case that requires growing the per-module metadata, and it is the trigger for this refinement.

This proposal addresses both drawbacks by representing module metadata as dynamic fields keyed by metadata kind. New kinds are added by writing a new key; readers fetch only the keys they need; and the PackageMetadata object type stays stable.

Specification

This proposal modifies only the publish/upgrade object-creation phase and the runtime accessors of the Package Metadata model. The compilation phase, the trust model, and the package-metadata object id derivation are unchanged from IIP-0010: metadata is still extracted from verified bytecode by the protocol, the object is still frozen, and its id is still derived from the package storage id.

Layout

The protocol builds PackageMetadataV1 with the following dynamic-field layout instead of the inline modules_metadata map.

  1. A metadata-layout version marker under the PackageMetadataVersionFieldName key (starting from 2, in order to distinguish from the implementation proposed for IIP-0010). Its presence allows to understand which layout a developer must reference to.
  2. A modules map under the ModulesMetadataFieldName key, of type VecMap<ModuleName, ModuleMetadata>, mapping each module name to its ModuleMetadata object.

The inline modules_metadata: VecMap<ascii::String, ModuleMetadataV1> field of PackageMetadataV1 is left empty under this layout; it is deprecated and MUST NOT be read by new code (it is kept in the struct definition for backwards compatibility).

Each ModuleMetadata is a standalone object that behaves as a key-value store backed by dynamic fields:

public struct ModuleMetadata has key, store {
    /// the ID of this module_metadata
    id: UID,
    /// the number of key-value pairs in the module_metadata
    size: u64,
}

Each entry in a ModuleMetadata is a dynamic field whose key type identifies a kind of metadata and whose value holds that kind’s payload. The protocol defines the following keys for this version:

  • ModuleMetadataV1FieldNameModuleMetadataV1 — the authenticator metadata from IIP-0010, re-homed under this key. Present only for modules that have at least one authenticator.
  • ViewFunctionMetadataV1FieldNamevector<ascii::String> — the list of view function names declared in the module (new; see View function metadata). Present for every module recorded under this layout.

Additional kinds can be introduced later as new key types without changing ModuleMetadata, PackageMetadataV1, or any existing key’s payload.

Module metadata object id derivation

Each ModuleMetadata object id is derived deterministically from the owning package storage id and the module name, reusing the same derived-object mechanism IIP-0010 uses for the package metadata object id:

#![allow(unused)]
fn main() {
module_metadata_id
= derive_object_id(package_storage_id, <0x2::module_metadata::ModuleMetadataKey>, module_name)
}

Where ModuleMetadataKey wraps the module name (ModuleMetadataKey(ascii::String)). Deriving from the package id together with the module name guarantees that:

  • each module of a package gets a distinct ModuleMetadata object, and
  • the same module name in different packages does not collide.

As with the package metadata id, this approach has the advantage that the id can be computed by off-chain tooling without a lookup.

View function metadata

A function annotated with #[view] (IIP-0011) is recorded in its module’s ModuleMetadata under the ViewFunctionMetadataV1FieldName key as a vector<ascii::String> of function names.

The flow mirrors the authenticator flow of IIP-0010:

  1. Compilation: the #[view] attribute is recorded in the function’s RuntimeModuleMetadata and embedded in the module bytecode, exactly as other recognized attributes are.
  2. Publish/upgrade verification: the IOTA bytecode verifier validates each function marked as a view function against the IIP-0011 signature constraints. A package that marks a non-conforming function as #[view] fails to publish — this is what lets readers trust the recorded list without re-checking signatures.
  3. Object creation: for each module, the protocol writes the verified view function names into the module’s ModuleMetadata.

Move types and methods

New accessors

/// Borrows the `ModuleMetadata` of the module named `module_name`.
/// Aborts with `EModuleMetadataNotFound` if the package has no metadata for that module.
public fun module_metadata(
    self: &PackageMetadataV1,
    module_name: &ascii::String,
): &ModuleMetadata;

/// Borrows the `AuthenticatorMetadataV1` of `function_name` in `module_name`.
public fun module_authenticator_function_metadata_v1(
    self: &PackageMetadataV1,
    module_name: &ascii::String,
    function_name: &ascii::String,
): &AuthenticatorMetadataV1;

/// Borrows / safely gets the `AuthenticatorMetadataV1` of `function_name`
/// from a given module metadata.
public fun authenticator_function_metadata_v1(
    self: &ModuleMetadata,
    function_name: &ascii::String,
): &AuthenticatorMetadataV1;
public fun try_get_authenticator_function_metadata_v1(
    self: &ModuleMetadata,
    function_name: &ascii::String,
): Option<AuthenticatorMetadataV1>;

On the ModuleMetadata object:

/// Returns true iff the module recorded any view function metadata.
public fun contains_view_functions_metadata_v1(self: &ModuleMetadata): bool;

/// Borrows the list of view function names recorded for the module.
public fun borrow_view_functions_metadata_v1(self: &ModuleMetadata): &vector<ascii::String>;

/// Returns true iff `function_name` is one of the module's view functions.
public fun is_view_function_v1(self: &ModuleMetadata, function_name: &ascii::String): bool;

/// Number of metadata kinds recorded for the module / whether it is empty.
public fun length(self: &ModuleMetadata): u64;
public fun is_empty(self: &ModuleMetadata): bool;

Deprecated accessors

The IIP-0010 accessors that return ModuleMetadataV1 by value are retained but deprecated. They are made layout-aware: when called on an object built with the dynamic-field layout (version marker == 2), they read through the dynamic fields; otherwise they fall back to the inline map. This keeps existing callers working across both layouts.

#[deprecated]
public fun try_get_modules_metadata_v1(
    self: &PackageMetadataV1,
    module_name: &ascii::String,
): Option<ModuleMetadataV1>;

#[deprecated]
public fun modules_metadata_v1(
    self: &PackageMetadataV1,
    module_name: &ascii::String,
): &ModuleMetadataV1;

Rationale

Why dynamic fields instead of struct versioning. IIP-0010 proposed accommodating new metadata kinds by bumping the PackageMetadata/ModuleMetadata struct version. Dynamic fields keyed by metadata kind achieve the same extensibility without type churn: a new kind is a new key, not a new struct type, and the PackageMetadata object type stays stable across additions. Readers fetch only the kinds they care about, and unknown kinds are simply absent keys rather than version mismatches. This matches how the metadata is actually consumed — an open, optional, per-module set of facts — better than a fixed, ordered struct.

Why a version marker. The PackageMetadataVersionFieldName field (value 2) lets a reader, and the deprecated accessors, distinguish the dynamic-field layout from the legacy inline layout on an existing object, so both can coexist on-chain after the protocol upgrade.

Why per-module derived objects. Giving each module its own ModuleMetadata object, with an id derived from the package id and module name, keeps each module’s dynamic fields in an isolated namespace, makes the module metadata object addressable and computable off-chain, and avoids collisions between identically named modules in different packages.

Trust model unchanged. As in IIP-0010, all content is written by the protocol from verified bytecode and frozen. For view functions specifically, the verifier rejects any package that marks a non-conforming function #[view], so the recorded view function list is trustworthy without re-validation by readers.

Backwards Compatibility

This proposal is backwards compatible.

  1. Existing metadata objects: packages published under the IIP-0010 inline layout keep their existing PackageMetadataV1 objects unchanged. They have no version marker and no dynamic fields, and the deprecated accessors continue to read them via the inline map.
  2. Accessor compatibility: the IIP-0010 accessors (modules_metadata_v1, try_get_modules_metadata_v1) remain available and are made layout-aware, so callers compiled against IIP-0010 continue to work against objects in either layout. They are deprecated in favor of the new module_metadata / *_function_metadata_v1 accessors.
  3. Adding future kinds: new module metadata kinds can be added as additional dynamic-field keys without changing any existing type or payload, and without bumping the layout version.

The modules_metadata inline field of PackageMetadataV1 is retained for compatibility but is empty under the new layout and deprecated for reads.

Test Cases

  1. Authenticator metadata round-trips through the dynamic-field layout (publish a package with #[authenticator] functions; read back via both the new and the deprecated accessors).
  2. View function metadata is recorded for #[view] functions and queryable via is_view_function_v1 / borrow_view_functions_metadata_v1.
  3. Conditional creation: a package with neither authenticators nor view functions produces no PackageMetadata object; a package with only view functions does.
  4. A package upgrade that removes a view function produces an updated metadata object for the new version while the previous version’s metadata stays valid.
  5. Deprecated accessors behave identically against legacy inline-layout objects and dynamic-field-layout objects.
  6. The bytecode verifier rejects a package that marks a non-conforming function as #[view] at publish time.

Reference Implementation

See PR #11616 for the reference implementation. Key components:

  • Framework modules iota::package_metadata and iota::module_metadata (crates/iota-framework/packages/iota-framework/sources/package_metadata/).
  • Protocol-side object creation under the feature flag (iota-execution/latest/iota-adapter/.../package_metadata.rs).
  • View function bytecode verification (iota-execution/latest/iota-verifier/.../view_function_verifier.rs).
  • Feature flag package_metadata_with_dynamic_module_metadata in crates/iota-protocol-config.

See also IIP-0010 (Package Metadata) and IIP-0011 (Core Move View Functions).

Copyright and related rights waived via CC0.