Imaging
Most lifecycle and relation-related signals cause a revision bump of the object, triggering the recalculation of its assets for the new revision. This process, known as object imaging (or simply imaging), is handled by the OVM on the Previous chain. Imaging is done by executing WebAssembly (Wasm) code defined in the object's kind contract.
Input/Output
Two key inputs are used to determine the code and data required for execution:
- oid: The object’s unique identifier.
- rev: The revision of the object.
The output is a vector, with each element consisting of:
- selector: Identifies the specific asset of the object.
- material: Represents the asset content of the object at the specified revision.
Selectors are extracted from the custom section (according to OVM specification) of the Wasm bytecode and correspond to public asset functions defined in the kind contract source code.
Execution
1. Loading the Kind Contract
The first step is loading the kind contract from chain storage. The code element of the kind object is used to retrieve the Wasm bytecode.
Pseudocode for retrieving the kind contract:
#![allow(unused)] fn main() { let kind_oid: Oid = oid.kind_oid(); let meta: Meta = storage.get_meta(oid, rev); let kind_elems: Vec<Bytes32> = storage.get_object_data(kind_oid, meta.kind_rev); let kind_code: Bytes32 = kind_elems[1]; let code_material: Material = storage.get_material(kind_code); let code: Bytes = code_material.content; }
2. Linking with Host Functions
The OVM specification requires a set of host functions to be available for the Wasm contract at runtime. These functions are critical for interacting with the blockchain state and for processing various types of materials.
Host functions handle materials such as:
- 2D images
- JSON data
- Merkle trees
- Plain tables
Other host functions provide access to object-related information, such as:
- Listing related objects: Helps retrieve the objects linked to the current object being processed.
- Fetching asset materials of related objects: Enables access to the assets of linked objects, crucial for interoperability.
New host functions can be added as the specification evolves. All host functions are designed to ensure determinism, which is essential for the security and verifiability of dynamic NFTs in a decentralized context.
3. Initializing Memory
The elements of the object are retrieved from chain storage and used to initialize the linear memory of the Wasm instance. Elements data is always starting from a fixed offset.
Pseudocode for loading the elements:
#![allow(unused)] fn main() { let elems: Vec<Bytes32> = storage.get_object_data(oid, rev); }
4. Invoking the Entry Function
The only function exported from a kind contract is the entry function. It is auto-generated by compilers to simplify the programming process. A selector is used to specify which asset function to call for each execution.
Pseudocode for entry function invacations:
#![allow(unused)] fn main() { let entry = instance.get_typed_func::<(u32, u32), Option<Rooted<ExternRef>>>(&mut store, "entry")?; let mut result = Vec::<ObjectAsset>::new(); for sel in selectors { let r = entry.call(&mut store, (OBJ_OFFSET, sel))?; let oa = Self::to_asset(sel, er, store)?; result.push(oa); } }
In theory, all asset functions need to be called to compute a new revision of an object, but the OVM can optimize this to only recalculate assets when necessary.
State Transition
Once execution succeeds, the generated assets are committed to chain storage, and a record of asset pointers (hashes to asset materials) is added to the object’s state for that revision. If the execution fails, no asset material is stored, and a record marking the object as malformed is saved.
This ensures that the assets of objects are accessible via satellites.