Sui.

Bài viết

Chia sẻ kiến thức của bạn.

Dpodium.js.
Aug 23, 2025
Hỏi đáp Chuyên Gia

Safe NFT Minting and Ownership Tracking

Given Sui’s object-centric model, how can I implement a dynamic NFT collection where each NFT evolves over time, ensuring that state mutations are safe, atomic, and efficiently traceable without redundant storage writes?

  • Sui
  • Architecture
  • SDKs and Developer Tools
1
8
Chia sẻ
Bình luận
.

Câu trả lời

8
Kurosaki.ether.
Aug 23 2025, 13:07

✨ Title: Safe NFT Minting and Ownership Tracking 🎨🔐

Answer: For dynamic NFTs on Sui while keeping state safe and traceable:

  1. 📦 Object-Centric Storage: Treat each NFT as a unique object with its own ID 🆔. Don’t lump multiple versions together — avoids messy conflicts.
  2. ⚡ Atomic Updates: Wrap NFT state changes in a single transaction ✅. That way, changes are all or nothing — no half-broken NFTs!
  3. 🔄 Versioning Traits: Keep evolving traits (⭐ level, 🎭 personality, ⚔️ experience) in optional fields or sub-objects. Makes updates smooth and safe.
  4. 📡 Event Emission: Emit events 🔔 for every state change. Off-chain tools and UIs can then track your NFT’s journey easily 📊.
  5. ✂️ Smart Updates: Only write what changes — no redundant reads/writes 🚀. For coins, lean on Move’s built-in split/merge magic.
  6. 🔑 Access Control: Use capabilities 🛡️ or admin keys 🔐 to ensure only the right actors can mutate NFT state. Keeps bad actors away 😎.

✨ Result → You get secure, traceable, and evolving NFTs that scale safely across the Sui ecosystem 🌍💎.

1
Bình luận
.
Dpodium.js.
Aug 23 2025, 16:17

Thanks a bunch, this was really helpful. Cheers! ! !

Gifted.eth.
Aug 23 2025, 13:13

Use Sui’s object model: represent each NFT as an owned object so state changes are atomic and tied to the owner. For evolution, store metadata inside the NFT object and update it directly instead of duplicating storage. Emit events for history/traceability rather than writing redundant state. This ensures safe mutations, atomic ownership tracking, and efficient updates without bloating storage.

0
Bình luận
.
casey.
Aug 23 2025, 13:37

Here’s a battle-tested pattern for “evolving” NFTs on Sui that keeps mutations safe and atomic, and makes the history easy to trace without writing big blobs over and over.

Safe evolving NFTs on Sui

  1. Split identity vs. state:
  • Nft = small mutable object (owner, version, pointer to current state).
  • NftState = immutable child per evolution (URI, traits, hashes).
  1. Evolve with caps + version check:
  • Require an EvolveCap (auth).
  • Check expected_version to prevent race conditions.
  • In one transaction: create new state, bump version, update pointer.
  1. Traceability:
  • Emit an Evolved event (nft_id, old_state, new_state, version, timestamp).
  • Optionally store a History dynamic field (version → state_id) for on-chain lookup.
  1. Efficiency:
  • Parent stays tiny (just version + pointer).
  • Heavy metadata only ever written once per state.
  • No redundant rewrites, only one new object + one small update per evolution.

Result: Evolutions are atomic, safe, cheap, and fully traceable through events (and history map if needed).

Here’s a minimal Move API sketch for safe evolving NFTs on Sui:

Core structs

move


struct Nft has key, store {
    id: UID,
    current_state: ID,
    version: u64,
}

struct NftState has key, store {
    id: UID,
    nft_id: ID,
    version: u64,
    uri: vector<u8>,        // or hash/traits
}

struct EvolveCap has key, store {
    id: UID,
    nft_id: ID,
}

struct Evolved has copy, drop {
    nft_id: ID,
    old_state: ID,
    new_state: ID,
    version: u64,
    sender: address,
}

Mint

move


public entry fun mint(
    uri: vector<u8>,
    recipient: address,
    ctx: &mut TxContext
) {
    let nft = Nft { id: object::new(ctx), current_state: object::id(ctx), version: 0 };
    let state = NftState { id: object::new(ctx), nft_id: object::uid_to_inner(&nft.id), version: 0, uri };
    let cap = EvolveCap { id: object::new(ctx), nft_id: object::uid_to_inner(&nft.id) };

    transfer::public_transfer(nft, recipient);
    transfer::public_transfer(state, recipient);
    transfer::public_transfer(cap, recipient);
}

Evolve

move


public entry fun evolve(
    nft: &mut Nft,
    cap: &EvolveCap,
    expected_version: u64,
    new_uri: vector<u8>,
    ctx: &mut TxContext
) {
    assert!(cap.nft_id == object::uid_to_inner(&nft.id), 0);
    assert!(nft.version == expected_version, 1);

    let new_state = NftState {
        id: object::new(ctx),
        nft_id: object::uid_to_inner(&nft.id),
        version: nft.version + 1,
        uri: new_uri,
    };

    let old_state = nft.current_state;
    nft.current_state = object::uid_to_inner(&new_state.id);
    nft.version = nft.version + 1;

    emit Evolved {
        nft_id: object::uid_to_inner(&nft.id),
        old_state,
        new_state: object::uid_to_inner(&new_state.id),
        version: nft.version,
        sender: tx_context::sender(ctx),
    };

    transfer::public_transfer(new_state, tx_context::sender(ctx));
}

This gives you:

  • Safe minting (NFT + state + cap)
  • Secure evolution (auth via cap, version check for atomicity)
  • Traceability (events, no redundant storage writes)
0
Bình luận
.
theking.
Aug 23 2025, 14:31

On Sui, designing a safe and efficient dynamic NFT collection means leveraging its object-centric model and Move’s resource safety to ensure each NFT evolves predictably while keeping gas and storage costs low. Here’s how you can approach it:


1. Use Owned Objects for Each NFT

Each NFT should be its own owned object with a unique UID. This ensures that ownership is cryptographically tied to a single address and any mutation (like level-ups, trait changes, or evolution) can only happen with the owner’s consent. Owned objects also benefit from parallel execution in Sui, so multiple NFT updates can happen at the same time without bottlenecks.

struct DynamicNFT has key, store {
    id: UID,
    name: String,
    level: u64,
    traits: vector<String>,
}

2. Atomic State Mutations

All mutations should be implemented as entry functions that consume a mutable reference to the NFT object. Because Sui enforces single ownership at the type level, you avoid issues like double-spends or race conditions. If a mutation fails (e.g., upgrade requirements aren’t met), the transaction reverts atomically, leaving the NFT unchanged.

public entry fun evolve_nft(nft: &mut DynamicNFT, new_trait: String) {
    nft.level = nft.level + 1;
    vector::push_back(&mut nft.traits, new_trait);
}

3. Efficient Traceability

Instead of rewriting the whole object history on-chain, rely on transaction logs and object versions. Every time an NFT mutates, Sui automatically increments the object version. You can query an NFT by its objectId to see its current state, and explorers or indexers can trace past versions. This makes evolution auditable without bloating storage.

sui client object <OBJECT_ID>

4. Avoid Redundant Storage Writes

Keep your NFT object lightweight. Store only mutable fields that change over time (like level, traits, or power). Metadata such as artwork, base attributes, or creator details should be stored in an immutable object or off-chain (IPFS/Arweave) with just a reference in the NFT. This minimizes storage gas fees because only the small dynamic fields are updated during evolution.


5. Shared vs. Owned Objects

If you want collection-wide logic (like a global “arena” where NFTs can evolve by battling), use a shared object for the arena, but still let each NFT remain owned. This keeps most mutations cheap and parallelizable while only the shared state (arena outcomes) goes through consensus.


6. Best Practices

  • Emit events on every mutation so off-chain indexers can track evolution without constantly reading state.
  • Use capabilities (special objects granted to creators/admins) to control minting or privileged updates.
  • Always test with dev-inspect and dry-run to estimate gas before rolling out large-scale collections.

In summary: Model each NFT as an owned object, keep it lightweight, let Sui’s versioning track mutations, and use events for history. Shared state should be minimal. This approach ensures your dynamic NFTs evolve safely, atomically, and without unnecessary storage costs.

👉 Read more here: Sui NFTs and Object Model

0
Bình luận
.
yungrazac.
Aug 24 2025, 13:34

You model each NFT as an owned object with a clear capability for mutation so you can evolve state safely in one atomic call while keeping writes minimal. In Move you define an NFT object with key that holds immutable identity fields and a compact pointer to a separate traits object for evolving attributes so an update only rewrites the small traits object rather than the whole NFT. You gate every mutation with a capability such as CreatorCap or UpdateCap that the entry function must receive so only authorized code can change state. You keep updates atomic by writing a single public entry that consumes the traits object applies the change emits an event and returns the updated object so the version bump and the new fields land together or not at all. You make history easy to trace by emitting structured events for every evolution and by relying on Sui object versioning so explorers can reconstruct the timeline without storing redundant snapshots. You avoid redundant storage writes by touching only the traits object that changed and by using dynamic fields or small child objects for each mutable facet like level skin or metadata pointer so unrelated fields do not get rewritten. You keep ownership correct by never recreating the NFT object and by transferring it normally while updates only touch its child objects. A clean operational flow is mint the base NFT and traits object grant the minimal UpdateCap run evolve calls that mutate traits and emit events transfer the NFT as needed and query history through events and object versions.

0
Bình luận
.
Tucker.
Aug 24 2025, 14:10

Below is a practical pattern for dynamic NFTs on Sui that evolve safely, atomically, and with efficient, queryable history—without rewriting big blobs each time.

Design goals

Safety: Only authorized updaters can mutate; strong type-checked capabilities.

Atomicity: Each evolution is one transaction that updates state and emits an event.

Traceability: History is append-only via events and compact “diff” objects; no large rewrites.

Efficiency: Keep the NFT small; store deltas or content hashes; avoid growing vectors.


Core objects and capabilities

Collection { AdminCap } – owns policy and mints NFTs; AdminCap gates mint and policy updates.

EvolvableNft – an owned object (best parallelism and lowest contention) with a small mutable state.

NftMutatorCap – optional per-NFT capability that authorizes future evolutions (mint to owner, or escrow to a game/engine).

Dynamic fields (optional) – for per-NFT “attachments” like traits without resizing the root struct.

Events – append-only history; one event per evolution, carrying compact diff and new state hash.


Move module sketch

module 0xYourPkg::dyn_nft { use sui::object::{Self, UID, ID}; use sui::transfer; use sui::tx_context::TxContext; use std::option::{Self, Option}; use std::string::String; use std::vector;

/// Collection gates minting and policy changes.
struct Collection has key {
    id: UID,
    name: String,
    // policy knobs (who can evolve, rate limits, etc.) kept small
    allow_external_mutators: bool,
}

/// Capability to control the collection.
struct AdminCap has key { id: UID }

/// Per-NFT capability that authorizes mutate() calls.
struct NftMutatorCap has key {
    id: UID,
    nft: ID,
}

/// Minimal mutable state for the NFT; keep this tiny.
/// Large content lives off-chain; we store its content hash and a URI.
struct EvolvableNft has key, store {
    id: UID,
    collection: ID,
    name: String,
    version: u64,
    content_uri: String,      // e.g. IPFS/HTTP/Arweave
    content_hash: vector<u8>, // hash of rendered JSON/image
    // Optional pointer to dynamic field “bag” if you need many traits
    bag: Option<ID>,
}

/// Emitted on every evolution. Append-only audit trail.
struct NftEvolvedEvent has copy, drop {
    nft: ID,
    old_version: u64,
    new_version: u64,
    old_hash: vector<u8>,
    new_hash: vector<u8>,
    // optional compact "diff" descriptor (e.g., changed trait keys)
    diff_keys: vector<String>,
}

// -------------------- Collection setup --------------------

public entry fun create_collection(
    name: String,
    ctx: &mut TxContext
): (Collection, AdminCap) {
    let c = Collection { id: object::new(ctx), name, allow_external_mutators: false };
    let cap = AdminCap { id: object::new(ctx) };
    (c, cap)
}

public entry fun set_allow_external_mutators(
    c: &mut Collection,
    _cap: &AdminCap,
    allow: bool
) {
    c.allow_external_mutators = allow;
}

// -------------------- Minting --------------------

public entry fun mint_nft(
    c: &Collection,
    _cap: &AdminCap,
    name: String,
    content_uri: String,
    content_hash: vector<u8>,
    to: address,
    ctx: &mut TxContext
) {
    let nft = EvolvableNft {
        id: object::new(ctx),
        collection: object::id(c),
        name,
        version: 0,
        content_uri,
        content_hash,
        bag: Option::none<ID>(),
    };
    transfer::transfer(nft, to);
}

/// Mint with a per-NFT mutator capability (hand to owner or a game engine).
public entry fun mint_nft_with_cap(
    c: &Collection,
    _cap: &AdminCap,
    name: String,
    content_uri: String,
    content_hash: vector<u8>,
    recipient: address,
    ctx: &mut TxContext
) {
    let nft = EvolvableNft {
        id: object::new(ctx),
        collection: object::id(c),
        name,
        version: 0,
        content_uri,
        content_hash,
        bag: Option::none<ID>(),
    };
    let cap = NftMutatorCap { id: object::new(ctx), nft: object::id(&nft) };
    // Deliver both in one atomic PTB step if desired:
    transfer::transfer(cap, recipient);
    transfer::transfer(nft, recipient);
}

// -------------------- Evolution (state updates) --------------------

/// Safe, atomic evolution gated by either:
/// - possession of NftMutatorCap for this NFT, or
/// - collection policy allowing external mutators.
public entry fun evolve(
    nft: &mut EvolvableNft,
    maybe_cap: Option<&NftMutatorCap>,
    new_uri: String,
    new_hash: vector<u8>,
    diff_keys: vector<String>,
    ctx: &mut TxContext
) {
    // AuthZ: require either a valid cap tied to this NFT or policy allows it.
    if (!is_authorized(nft, &maybe_cap)) {
        abort 0; // replace with a proper error code table
    };

    let old_ver = nft.version;
    let old_hash = nft.content_hash;
    let _ = old_hash; // move checker friendliness; copy below

    nft.content_uri = new_uri;
    nft.content_hash = new_hash;
    nft.version = old_ver + 1;

    // Emit event as append-only history; no large vectors stored on-chain.
    emit NftEvolvedEvent {
        nft: object::id(nft),
        old_version: old_ver,
        new_version: nft.version,
        old_hash: old_hash,
        new_hash: vector::copy<nft.content_hash>(&nft.content_hash),
        diff_keys,
    };
}

fun is_authorized(nft: &EvolvableNft, maybe_cap: &Option<&NftMutatorCap>) : bool {
    match *maybe_cap {
        Option::Some(cap) => object::id(nft) == cap.nft,
        Option::None => false
    }
}

}

Why this is safe and efficient

Atomicity: The entry fun evolve mutates the NFT and emits the event in a single transaction.

Authorization: Controlled by a capability bound to a specific NFT ID; possession of the cap is the right to mutate.

No redundant writes: You mutate a few small fields and emit an event; you don’t append to vectors or maintain bulky history on-chain.

Traceability: Off-chain indexers or your backend can reconstruct full history from NftEvolvedEvent by nft ID.

Parallelism: Keeping NFTs owned (not shared) avoids lock contention and leverages Sui’s parallel execution.


Optional extensions

  1. Dynamic fields for traits

If you need many mutable attributes, store them as dynamic fields keyed by String to avoid a growing vector in the root NFT.

Each trait update still emits an event; the NFT root remains small.

  1. Rate limiting / cooldowns

Add last_update_epoch and check against clock::epoch() for per-NFT throttling.

  1. Off-chain rendering with verifiable integrity

Commit to a content hash; your metadata server produces JSON/art that matches that hash.

Clients verify content_hash before displaying.

  1. Role-based evolution

Instead of per-NFT NftMutatorCap, issue CollectionMutatorCap to trusted updaters that can mutate any NFT in the collection.

  1. Freezing

Add a frozen: bool flag; only AdminCap can toggle. If frozen, evolve aborts.


Frontend/PTB example (TypeScript)

import { Transaction, SuiClient, getFullnodeUrl } from "@mysten/sui.js";

const client = new SuiClient({ url: getFullnodeUrl("mainnet") });

export async function evolveNft({ packageId, nftId, capId, // optional: if using NftMutatorCap newUri, newHashHex, diffKeys, sender, }) { const tx = new Transaction(); tx.setSender(sender);

const args = [ tx.object(nftId), capId ? tx.optionSome(tx.object(capId)) : tx.optionNone("0x1::option::Option<0xYourPkg::dyn_nft::NftMutatorCap>"), tx.pure.string(newUri), tx.pure.vector("u8", Array.from(Buffer.from(newHashHex.replace(/^0x/, ""), "hex"))), tx.pure.vector("string", diffKeys), ];

tx.moveCall({ target: ${packageId}::dyn_nft::evolve, arguments: args, });

// Simulate for gas + effects preview const dry = await client.dryRunTransaction({ transactionBlock: await tx.build({ client }) });

// Submit if dry run looks good // const res = await client.signAndExecuteTransaction({ transaction: tx, signer }); return dry; }


Common pitfalls and how to avoid them

Using shared NFTs unnecessarily: Shared objects serialize access and add contention. Prefer owned NFTs unless many parties must concurrently mutate the same object.

Storing full history in the NFT: Leads to ever-growing vectors and gas spikes. Emit events; keep the object small.

Missing capability gating: Always bind mutator capability to the NFT’s ID to prevent cross-token misuse.

Large string/JSON diffs in state: Put large payloads off-chain; store only a hash and small diff keys.

Overwriting without versioning: Increment version per change and include it in events for auditability.


Querying history (indexer or backend)

Filter NftEvolvedEvent by nft ID to reconstruct timeline.

Keep a denormalized table keyed by (nft_id, version) with content_uri, content_hash, timestamp, tx_digest.

Verify client-rendered content by comparing its hash to content_hash.


When to consider shared objects

If evolution requires multiple signers in the same transaction or a global rule engine must coordinate many NFTs atomically, introduce a shared “engine” object that consumes a capability and mutates multiple owned NFTs in one PTB. Keep the engine’s footprint minimal

0
Bình luận
.
Jeff.
Jeff1213
Aug 24 2025, 14:13

Here’s a different, production-ready pattern that keeps NFT state small, evolutions atomic, and history precise—by pushing history into append-only version objects instead of rewriting the NFT.

High-level architecture

CollectionController { AdminCap }: governs minting, evolution policy, and emergency controls.

EvoNft (owned): the tiny “handle” users trade; it stores only collection_id, name, version, and a pointer to the current Version object ID.

Version (owned by the controller, referenced by the NFT): immutable, append-only object with parent_version, content_uri, content_hash, and optional compact diff metadata.

Dynamic fields under the CollectionController map nft_id → latest_version_id for O(1) lookups without bloating the NFT.

Events emitted only for pointer changes, not for large payloads.

This gives you:

Safety: Only the controller can create new versions and atomically swing the NFT pointer.

Atomicity: Create Version, update pointer, emit event all in one entry.

Traceability: History is a linked list of Version objects plus events; no redundant rewrites.

Efficiency: The traded NFT object is tiny; large content lives off-chain and is referenced by hash.


Move sketch

module 0xPkg::evo_nft { use sui::object::{Self, UID, ID}; use sui::tx_context::TxContext; use sui::transfer; use sui::dynamic_field::{Self as df, Field}; use std::string::String; use std::vector;

/// Controller for a collection; owns the version index and policy.
struct CollectionController has key {
    id: UID,
    name: String,
    // per-collection index: nft_id -> current version id
    index_bag: ID,
    evolvable: bool,
}

struct AdminCap has key { id: UID }

/// The minimal, tradable NFT.
struct EvoNft has key, store {
    id: UID,
    collection: ID,
    name: String,
    version: u64,
    // redundancy: current_version_id also tracked in controller index
    current_version: ID,
}

/// Immutable version node; append-only history.
struct Version has key, store {
    id: UID,
    nft: ID,
    parent: Option<ID>,
    version: u64,
    content_uri: String,
    content_hash: vector<u8>,   // hash of rendered metadata/art
    diff_keys: vector<String>,  // compact change description
}

struct Evolved has copy, drop {
    nft: ID,
    old_version: u64,
    new_version: u64,
    new_version_id: ID,
}

// ---------- Setup ----------
public entry fun create_collection(name: String, ctx: &mut TxContext)
    : (CollectionController, AdminCap)
{
    let bag = object::new(ctx);
    (CollectionController { id: object::new(ctx), name, index_bag: bag, evolvable: true },
     AdminCap { id: object::new(ctx) })
}

public entry fun set_evolvable(c: &mut CollectionController, _cap: &AdminCap, on: bool) {
    c.evolvable = on;
}

// ---------- Mint ----------
public entry fun mint(
    c: &mut CollectionController,
    _cap: &AdminCap,
    name: String,
    content_uri: String,
    content_hash: vector<u8>,
    to: address,
    ctx: &mut TxContext
) {
    let nft = EvoNft {
        id: object::new(ctx),
        collection: object::id(c),
        name,
        version: 0,
        current_version: object::id_raw(0), // temp; patched below
    };

    // create first version (no parent)
    let v0 = Version {
        id: object::new(ctx),
        nft: object::id(&nft),
        parent: Option::none<ID>(),
        version: 0,
        content_uri,
        content_hash,
        diff_keys: vector::empty<String>(),
    };
    let v0_id = object::id(&v0);
    // write index: nft_id -> v0_id
    df::add(&mut c.id, &nft.id, v0_id);

    // patch nft pointer then transfer
    let mut nft_mut = nft;
    nft_mut.current_version = v0_id;
    transfer::transfer(nft_mut, to);
    transfer::transfer(v0, to); // or keep versions under controller if desired
}

// ---------- Evolve (atomic) ----------
public entry fun evolve(
    c: &mut CollectionController,
    _cap: &AdminCap,
    nft: &mut EvoNft,
    new_uri: String,
    new_hash: vector<u8>,
    diff_keys: vector<String>,
    ctx: &mut TxContext
) {
    assert!(c.evolvable, 1);

    // look up current version id from controller's index
    let current_id = df::borrow<ID, ID>(&c.id, &nft.id);
    let parent_id = *current_id;

    // create new immutable Version
    let v = Version {
        id: object::new(ctx),
        nft: object::id(nft),
        parent: Option::some<ID>(parent_id),
        version: nft.version + 1,
        content_uri: new_uri,
        content_hash: new_hash,
        diff_keys,
    };
    let v_id = object::id(&v);

    // atomically swing pointers
    nft.version = nft.version + 1;
    nft.current_version = v_id;
    df::remove<ID, ID>(&mut c.id, &nft.id);
    df::add<ID, ID>(&mut c.id, &nft.id, v_id);

    emit Evolved { nft: object::id(nft), old_version: nft.version - 1, new_version: nft.version, new_version_id: v_id };

    // choose version custody: keep under owner to enable easy resale with history,
    // or transfer v back to controller to centralize storage; here we keep with owner:
    transfer::transfer(v, transfer::recipient_address_of(nft));
}

}

Notes

The NFT is tiny; history lives in immutable Version nodes.

The controller’s dynamic field index provides O(1) “latest pointer” without growing the NFT.

Every evolution is a single entry that creates Version, flips the pointer, emits Evolved.


Why this meets your goals

Safety: Only AdminCap can evolve; you can extend with per-NFT caps or role lists if needed.

Atomicity: No intermediate states leak; pointer flips and event emission happen together.

Traceability: Walk the Version.parent chain or subscribe to Evolved events; no redundant writes.

Gas efficiency: You never rewrite big vectors on the NFT; new data is an immutable, compact object.


Operational tips

Off-chain integrity: Clients fetch content_uri and verify content_hash before rendering.

GC policy: Optionally add a prune_versions admin entry that consolidates historical versions into a Merkle root and stores only the root hash to cap on-chain bloat.

Rate limits: Add last_update_epoch on NFT or controller, checked against clock::epoch().


PTB outline (TypeScript)

import { SuiClient, Transaction, getFullnodeUrl } from "@mysten/sui.js";

const client = new SuiClient({ url: getFullnodeUrl("mainnet") });

export async function evolve({ pkg, controllerId, adminCapId, nftId, newUri, newHashHex, sender, }) { const tx = new Transaction(); tx.setSender(sender); tx.moveCall({ target: ${pkg}::evo_nft::evolve, arguments: [ tx.object(controllerId), tx.object(adminCapId), tx.object(nftId), tx.pure.string(newUri), tx.pure.vector("u8", Array.from(Buffer.from(newHashHex.replace(/^0x/, ""), "hex"))), tx.pure.vector("string", []), ], }); const dry = await client.dryRunTransaction({ transactionBlock: await tx.build({ client }) }); return dry; }


Common pitfalls avoided

No shared NFTs unless required: keeps parallelism high.

No ever-growing arrays in the NFT: avoids gas spikes.

Clear capability gating via AdminCap; trivially extendable to per-NFT mutator caps.

History is immutable: tamper-evident and easy to index.

This variant emphasizes append-only version objects plus a lightweight pointer in the NFT, achieving safe evolution, atomicity, minimal writes, and clean auditability.

0
Bình luận
.
JK spike.
Aug 24 2025, 14:17

Design overview

Collection { AdminCap }: shared controller that owns policy and a CheckpointRegistry.

Nft (owned): minimal handle with state_nonce, checkpoint_root, and policy_id. No large vectors on the NFT.

JournalEntry (owned by sender during tx): ephemeral object carrying proposed changes plus their Merkle leaf hash.

Checkpoint (owned by collection): immutable commitment node storing the Merkle root of all entries in that evolution batch, parent root, and epoch.

Dynamic fields under Collection map nft_id → latest_checkpoint_root for constant-time lookups.

Events: NftEvolved emits the old/new roots and nonce; off-chain indexers fetch deltas by root.

This gives:

Atomicity: verify leaf membership → update nonce → swing root → emit event in one entry.

Safety: only AdminCap (or per-NFT MutatorCap) can attest a checkpoint; leaf data is validated by hash.

Efficiency: the NFT stores two words; all history is in immutable checkpoints.

Traceability: linear chain of checkpoint roots; reconstruct by following parent_root.


Move sketch

module 0xPkg::nft_commit { use sui::object::{Self, UID, ID}; use sui::tx_context::TxContext; use sui::dynamic_field as df; use sui::transfer; use sui::event; use std::option::Option; use std::vector;

struct Collection has key {
    id: UID,
    index_bag: ID,              // nft_id -> latest checkpoint root
    upgrades_allowed: bool,
}

struct AdminCap has key { id: UID }

struct Nft has key, store {
    id: UID,
    collection: ID,
    state_nonce: u64,           // anti-replay
    checkpoint_root: ID,        // current root (0x0 if genesis)
}

/// Immutable, append-only checkpoint.
struct Checkpoint has key, store {
    id: UID,
    parent: Option<ID>,
    merkle_root: ID,            // treat as opaque ID for root
    epoch: u64,
}

/// Ephemeral proposal (not stored long-term).
struct JournalEntry has store {
    nft: ID,
    leaf_hash: vector<u8>,
    nonce: u64,
}

struct NftEvolved has copy, drop {
    nft: ID,
    old_root: ID,
    new_root: ID,
    nonce: u64,
    epoch: u64,
}

public entry fun create_collection(ctx: &mut TxContext):(Collection, AdminCap) {
    (Collection { id: object::new(ctx), index_bag: object::new(ctx), upgrades_allowed: true },
     AdminCap { id: object::new(ctx) })
}

public entry fun mint(
    c: &mut Collection, _cap: &AdminCap, to: address, ctx: &mut TxContext
) {
    let nft = Nft {
        id: object::new(ctx),
        collection: object::id(c),
        state_nonce: 0,
        checkpoint_root: object::id_raw(0),
    };
    // index starts at zero root
    df::add<ID, ID>(&mut c.id, &nft.id, object::id_raw(0));
    transfer::transfer(nft, to);
}

/// Off-chain prepares a merkle tree; on-chain we only verify root and leaf hash binding.
public entry fun evolve_with_checkpoint(
    c: &mut Collection,
    _cap: &AdminCap,
    nft: &mut Nft,
    leaf: JournalEntry,
    new_merkle_root: ID,
    epoch: u64,
    parent_root_opt: Option<ID>,
    ctx: &mut TxContext
) {
    assert!(c.upgrades_allowed, 1);
    // anti-replay
    assert!(leaf.nonce == nft.state_nonce + 1, 2);
    assert!(leaf.nft == object::id(nft), 3);

    // Optional: check parent_root matches current index
    let cur = df::borrow<ID, ID>(&c.id, &nft.id);
    let expected_parent = match parent_root_opt {
        Option::Some(r) => r,
        Option::None => object::id_raw(0),
    };
    assert!(*cur == expected_parent, 4);

    // Create immutable checkpoint
    let cp = Checkpoint {
        id: object::new(ctx),
        parent: parent_root_opt,
        merkle_root: new_merkle_root,
        epoch,
    };
    let cp_id = object::id(&cp);

    // Swing pointers atomically
    nft.state_nonce = leaf.nonce;
    nft.checkpoint_root = cp_id;
    df::remove<ID, ID>(&mut c.id, &nft.id);
    df::add<ID, ID>(&mut c.id, &nft.id, cp_id);

    event::emit(NftEvolved {
        nft: object::id(nft),
        old_root: expected_parent,
        new_root: cp_id,
        nonce: nft.state_nonce,
        epoch,
    });

    // Keep checkpoint with owner for discoverability
    transfer::transfer(cp, transfer::recipient_address_of(nft));
    // drop ephemeral leaf (no storage write)
    let _ = leaf;
}

}

Why this is safe and efficient

The NFT’s nonce blocks replayed leaves.

The root swap and event emission are a single atomic entry.

No large fields mutate on the NFT; history stays in immutable checkpoints.

Off-chain merkle proofs bound updates to a specific NFT and nonce.


Off-chain flow

  1. Fetch nft.checkpoint_root and state_nonce.

  2. Build the next state off-chain; hash into a leaf commitment including nft_id and nonce+1.

  3. Compute new_merkle_root; store tree off-chain (e.g., IPFS/DB).

  4. Submit JournalEntry { nft_id, leaf_hash, nonce+1 } and new_merkle_root, plus parent_root.

  5. Indexers watch NftEvolved events to link roots → full state snapshots.


PTB outline

const tx = new Transaction(); tx.moveCall({ target: ${pkg}::nft_commit::evolve_with_checkpoint, arguments: [ tx.object(collectionId), tx.object(adminCapId), tx.object(nftId), tx.moveCall({ // pack JournalEntry target: ${pkg}::nft_commit::new_journal_entry, arguments: [ tx.pure.id(nftId), tx.pure.vector("u8", leafHash), tx.pure.u64(nextNonce) ] }), tx.object(newMerkleRootId), // represent root as an object ID (or use ID literal) tx.pure.u64(epoch), parentRootId ? tx.object(parentRootId) : tx.pure.option("address", null), // ctx auto ], });


Operational notes

Integrity checks: bind leaf_hash = H(nft_id || nonce || state_delta) to prevent cross-NFT reuse.

Evolution policy: gate evolve_with_checkpoint by AdminCap or per-NFT MutatorCap to allow creator/owner-driven changes.

Compression: keep off-chain tree minimal; on-chain only stores root ID.

Audits: auditors can reconstruct exact state at any time by chasing roots and verifying merkle proofs.

This variant favors hash-committed checkpoints with nonce-based replay protection, giving you atomic updates, cheap writes, robust provenance, and clean parallelism without bloating the NFT object.

0
Bình luận
.

Bạn có biết câu trả lời không?

Hãy đăng nhập và chia sẻ nó.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

850Bài viết2235Câu trả lời
Sui.X.Peera.

Kiếm phần của bạn từ 1000 Sui

Tích lũy điểm danh tiếng và nhận phần thưởng khi giúp cộng đồng Sui phát triển.

Chiến dịch phần thưởngTháng Tám