diff --git a/iips/IIP-0010/iip-0010.md b/iips/IIP-0010/iip-0010.md new file mode 100644 index 0000000..d98cb4d --- /dev/null +++ b/iips/IIP-0010/iip-0010.md @@ -0,0 +1,336 @@ +--- +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](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0009/iip-0009.md)): 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](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0005/iip-0005.md)): 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](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0009/iip-0009.md)), 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](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0009/iip-0009.md)). + +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: + +```rust +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>` 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. + +```move +public fun create_auth_function_ref_v1( + package_metadata: &PackageMetadataV1, + module_name: ascii::String, + function_name: ascii::String, +): AuthenticatorFunctionRefV1 { + // 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() == authenticator_metadata.account_type(), + EAuthenticatorFunctionRefV1NotCompatibleWithAccount, + ); + AuthenticatorFunctionRefV1 { + package: package_metadata.storage_id(), + module_name, + function_name, + } +} +``` + +#### Move Types and Methods Specification + +##### Main Types: + +```move + +/// 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, +} + +/// 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, +} + +/// 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: + +```move +/// 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 { } + +/// 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 { } + +/// 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](https://github.com/iotaledger/IIPs/blob/main/iips/IIP-0009/iip-0009.md). + +## 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 + +| Attribute | Purpose | Metadata Fields | +| ------------------ | ---------------------- | --------------------------- | +| `#[authenticator]` | Account authentication | function_name, account_type | +| `#[view]` | Read-only functions | function_name, return_type | + +### Tooling + +- CLI: `iota package metadata ` +- GraphQL: Package metadata queries +- Explorer: Metadata visualization + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).