program card

// ======================================================
// ZK-Native Card โ€” TSP-2 (PLUMB)
// Ops: Pay (0), Lock (1), Update (2), Mint (3), Burn (4)
//
// PLUMB = Pay, Lock, Update, Mint, Burn
//
// State: Merkle tree of asset leaves + config commitment
//
// Leaf (10 fields):
//   hash(asset_id, owner_id, nonce, auth_hash, lock_until,
//        collection_id, metadata_hash, royalty_bps, creator_id, flags)
//
// Config (10 fields = 5 authorities + 5 hooks):
//   hash(admin_auth, pay_auth, lock_auth, mint_auth, burn_auth,
//        pay_hook, lock_hook, update_hook, mint_hook, burn_hook)
//
// Flags bitfield (5 bits):
//   bit 0 = TRANSFERABLE (Pay allowed)
//   bit 1 = BURNABLE     (Burn allowed)
//   bit 2 = UPDATABLE    (Metadata update allowed)
//   bit 3 = LOCKABLE     (Lock allowed)
//   bit 4 = MINTABLE     (Collection accepts new mints)
//   Flags are immutable after mint.
//
// Authorization:
//   Account ops (Pay, Lock, Burn): owner auth always required.
//     If config auth โ‰  0, dual auth (owner + config) required.
//   Config ops (Mint): config auth required. 0 = disabled.
//   Config ops (Update): admin auth required. 0 = renounced.
//
// Leaves are Merkle-authenticated against the state root.
// Hook program IDs are output for external proof composition.
// ======================================================
use std.crypto.merkle
use os.neptune.standards.plumb

// --- Flag constants ---
// TRANSFERABLE = 1, BURNABLE = 2, UPDATABLE = 4, LOCKABLE = 8, MINTABLE = 16
// --- Asset leaf hashing (10 fields) ---
fn hash_leaf(
    asset_id: Field,
    owner_id: Field,
    nonce: Field,
    auth_hash: Field,
    lock_until: Field,
    collection_id: Field,
    metadata_hash: Field,
    royalty_bps: Field,
    creator_id: Field,
    flags: Field
) -> Digest {
    hash(
        asset_id,
        owner_id,
        nonce,
        auth_hash,
        lock_until,
        collection_id,
        metadata_hash,
        royalty_bps,
        creator_id,
        flags
    )
}



// --- Flag checks ---
fn assert_transferable(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
    let f1: Field = sub(flags, 1) * sub(flags, 3) * sub(flags, 5) * sub(flags, 7)
    let f9: Field = sub(flags, 9) * sub(flags, 11) * sub(flags, 13) * sub(flags, 15)
    let f17: Field = sub(flags, 17) * sub(flags, 19) * sub(flags, 21) * sub(flags, 23)
    let f25: Field = sub(flags, 25) * sub(flags, 27) * sub(flags, 29) * sub(flags, 31)
    let valid: Field = f1 * f9 * f17 * f25
    assert_eq(valid, 0)
}

fn assert_burnable(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
    let f2: Field = sub(flags, 2) * sub(flags, 3) * sub(flags, 6) * sub(flags, 7)
    let f10: Field = sub(flags, 10) * sub(flags, 11) * sub(flags, 14) * sub(flags, 15)
    let f18: Field = sub(flags, 18) * sub(flags, 19) * sub(flags, 22) * sub(flags, 23)
    let f26: Field = sub(flags, 26) * sub(flags, 27) * sub(flags, 30) * sub(flags, 31)
    let valid: Field = f2 * f10 * f18 * f26
    assert_eq(valid, 0)
}

fn assert_updatable(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
    let f4: Field = sub(flags, 4) * sub(flags, 5) * sub(flags, 6) * sub(flags, 7)
    let f12: Field = sub(flags, 12) * sub(flags, 13) * sub(flags, 14) * sub(flags, 15)
    let f20: Field = sub(flags, 20) * sub(flags, 21) * sub(flags, 22) * sub(flags, 23)
    let f28: Field = sub(flags, 28) * sub(flags, 29) * sub(flags, 30) * sub(flags, 31)
    let valid: Field = f4 * f12 * f20 * f28
    assert_eq(valid, 0)
}

fn assert_lockable(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
    let f8: Field = sub(flags, 8) * sub(flags, 9) * sub(flags, 10) * sub(flags, 11)
    let f12: Field = sub(flags, 12) * sub(flags, 13) * sub(flags, 14) * sub(flags, 15)
    let f24: Field = sub(flags, 24) * sub(flags, 25) * sub(flags, 26) * sub(flags, 27)
    let f28: Field = sub(flags, 28) * sub(flags, 29) * sub(flags, 30) * sub(flags, 31)
    let valid: Field = f8 * f12 * f24 * f28
    assert_eq(valid, 0)
}

fn assert_mintable(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
    let low: Field = sub(flags, 16)
    plumb.assert_non_negative(low)
}

fn assert_valid_flags(flags: Field) {
    let _: U32 = as_u32(flags)
    let headroom: Field = sub(31, flags)
    let _: U32 = as_u32(headroom)
}

// --- Events ---
event Pay {
    asset_id: Field,
    from_owner: Field,
    to_owner: Field,
    royalty_bps: Field,
}

event Lock {
    asset_id: Field,
    lock_until: Field,
}

event MetadataUpdate {
    asset_id: Field,
    old_metadata: Field,
    new_metadata: Field,
}

event Mint {
    asset_id: Field,
    creator_id: Field,
    collection_id: Field,
    metadata_hash: Field,
}

event Burn {
    asset_id: Field,
    owner_id: Field,
}

event Nullifier {
    asset_id: Field,
    nonce: Field,
}

event SupplyChange {
    old_count: Field,
    new_count: Field,
}

// ============================================================
// Op 0: PAY โ€” transfer asset ownership
// ============================================================
fn pay() {
    let old_root: Digest = pub_read5()
    let new_root: Digest = pub_read5()
    let asset_count: Field = pub_read()
    let asset_id: Field = pub_read()
    let current_time: Field = pub_read()
    let config: Digest = pub_read5()
    // --- Verify config ---
    let cfg_admin: Field = divine()
    let cfg_pay: Field = divine()
    let cfg_lock: Field = divine()
    let cfg_mint: Field = divine()
    let cfg_burn: Field = divine()
    let cfg_pay_hook: Field = divine()
    let cfg_lock_hook: Field = divine()
    let cfg_update_hook: Field = divine()
    let cfg_mint_hook: Field = divine()
    let cfg_burn_hook: Field = divine()
    plumb.verify_config(
        cfg_admin,
        cfg_pay,
        cfg_lock,
        cfg_mint,
        cfg_burn,
        cfg_pay_hook,
        cfg_lock_hook,
        cfg_update_hook,
        cfg_mint_hook,
        cfg_burn_hook,
        config
    )
    // --- Current asset leaf ---
    let leaf_asset_id: Field = divine()
    let leaf_owner_id: Field = divine()
    let leaf_nonce: Field = divine()
    let leaf_auth_hash: Field = divine()
    let leaf_lock_until: Field = divine()
    let leaf_collection_id: Field = divine()
    let leaf_metadata_hash: Field = divine()
    let leaf_royalty_bps: Field = divine()
    let leaf_creator_id: Field = divine()
    let leaf_flags: Field = divine()
    // Authenticate leaf against old state root
    let old_leaf: Digest = hash_leaf(
        leaf_asset_id,
        leaf_owner_id,
        leaf_nonce,
        leaf_auth_hash,
        leaf_lock_until,
        leaf_collection_id,
        leaf_metadata_hash,
        leaf_royalty_bps,
        leaf_creator_id,
        leaf_flags
    )
    let leaf_idx: U32 = as_u32(divine())
    merkle.verify(old_leaf, old_root, leaf_idx, plumb.tree_depth())
    // Asset ID must match public input
    assert_eq(leaf_asset_id, asset_id)
    // Owner authorization
    plumb.verify_auth(leaf_auth_hash)
    // Dual auth if pay_auth โ‰  0
    if cfg_pay == 0 {
    } else {
        plumb.verify_auth(cfg_pay)
    }
    // Time-lock check
    let lock_headroom: Field = sub(current_time, leaf_lock_until)
    plumb.assert_non_negative(lock_headroom)
    // Flag check: must be transferable
    assert_transferable(leaf_flags)
    // --- New owner ---
    let new_owner_id: Field = divine()
    let new_auth_hash: Field = divine()
    // New leaf: only owner_id, auth_hash, nonce change
    let new_nonce: Field = leaf_nonce + 1
    let new_leaf: Digest = hash_leaf(
        leaf_asset_id,
        new_owner_id,
        new_nonce,
        new_auth_hash,
        leaf_lock_until,
        leaf_collection_id,
        leaf_metadata_hash,
        leaf_royalty_bps,
        leaf_creator_id,
        leaf_flags
    )
    // Authenticate new leaf against new state root
    merkle.verify(new_leaf, new_root, leaf_idx, plumb.tree_depth())
    // Hook signal
    plumb.signal_hook(cfg_pay_hook)
    // Nullifier (sealed โ€” prevents replay)
    seal Nullifier { asset_id: leaf_asset_id, nonce: leaf_nonce }
    // Events
    reveal
    Pay { asset_id: leaf_asset_id, from_owner: leaf_owner_id, to_owner: new_owner_id, royalty_bps: leaf_royalty_bps }
}

// ============================================================
// Op 1: LOCK โ€” time-lock an asset
// ============================================================
fn lock() {
    let old_root: Digest = pub_read5()
    let new_root: Digest = pub_read5()
    let asset_count: Field = pub_read()
    let asset_id: Field = pub_read()
    let lock_until_time: Field = pub_read()
    let config: Digest = pub_read5()
    // --- Verify config ---
    let cfg_admin: Field = divine()
    let cfg_pay: Field = divine()
    let cfg_lock: Field = divine()
    let cfg_mint: Field = divine()
    let cfg_burn: Field = divine()
    let cfg_pay_hook: Field = divine()
    let cfg_lock_hook: Field = divine()
    let cfg_update_hook: Field = divine()
    let cfg_mint_hook: Field = divine()
    let cfg_burn_hook: Field = divine()
    plumb.verify_config(
        cfg_admin,
        cfg_pay,
        cfg_lock,
        cfg_mint,
        cfg_burn,
        cfg_pay_hook,
        cfg_lock_hook,
        cfg_update_hook,
        cfg_mint_hook,
        cfg_burn_hook,
        config
    )
    // --- Current asset leaf ---
    let leaf_asset_id: Field = divine()
    let leaf_owner_id: Field = divine()
    let leaf_nonce: Field = divine()
    let leaf_auth_hash: Field = divine()
    let leaf_lock_until: Field = divine()
    let leaf_collection_id: Field = divine()
    let leaf_metadata_hash: Field = divine()
    let leaf_royalty_bps: Field = divine()
    let leaf_creator_id: Field = divine()
    let leaf_flags: Field = divine()
    // Authenticate leaf against old state root
    let old_leaf: Digest = hash_leaf(
        leaf_asset_id,
        leaf_owner_id,
        leaf_nonce,
        leaf_auth_hash,
        leaf_lock_until,
        leaf_collection_id,
        leaf_metadata_hash,
        leaf_royalty_bps,
        leaf_creator_id,
        leaf_flags
    )
    let leaf_idx: U32 = as_u32(divine())
    merkle.verify(old_leaf, old_root, leaf_idx, plumb.tree_depth())
    assert_eq(leaf_asset_id, asset_id)
    // Owner authorization
    plumb.verify_auth(leaf_auth_hash)
    // Dual auth if lock_auth โ‰  0
    if cfg_lock == 0 {
    } else {
        plumb.verify_auth(cfg_lock)
    }
    // Flag check: must be lockable
    assert_lockable(leaf_flags)
    // Lock can only extend, not shorten
    let extension: Field = sub(lock_until_time, leaf_lock_until)
    plumb.assert_non_negative(extension)
    // New leaf: only lock_until and nonce change
    let new_nonce: Field = leaf_nonce + 1
    let new_leaf: Digest = hash_leaf(
        leaf_asset_id,
        leaf_owner_id,
        new_nonce,
        leaf_auth_hash,
        lock_until_time,
        leaf_collection_id,
        leaf_metadata_hash,
        leaf_royalty_bps,
        leaf_creator_id,
        leaf_flags
    )
    // Authenticate new leaf against new state root
    merkle.verify(new_leaf, new_root, leaf_idx, plumb.tree_depth())
    // Hook signal
    plumb.signal_hook(cfg_lock_hook)
    reveal
    Lock { asset_id: leaf_asset_id, lock_until: lock_until_time }
}

// ============================================================
// Op 2: UPDATE โ€” update config or asset metadata
// ============================================================
// Two modes:
//   Config update: old_root == new_root, admin auth required
//   Metadata update: owner auth + UPDATABLE flag required
fn update() {
    let old_root: Digest = pub_read5()
    let new_root: Digest = pub_read5()
    let asset_count: Field = pub_read()
    let asset_id: Field = pub_read()
    let new_metadata_hash: Field = pub_read()
    let config: Digest = pub_read5()
    // --- Verify config ---
    let cfg_admin: Field = divine()
    let cfg_pay: Field = divine()
    let cfg_lock: Field = divine()
    let cfg_mint: Field = divine()
    let cfg_burn: Field = divine()
    let cfg_pay_hook: Field = divine()
    let cfg_lock_hook: Field = divine()
    let cfg_update_hook: Field = divine()
    let cfg_mint_hook: Field = divine()
    let cfg_burn_hook: Field = divine()
    plumb.verify_config(
        cfg_admin,
        cfg_pay,
        cfg_lock,
        cfg_mint,
        cfg_burn,
        cfg_pay_hook,
        cfg_lock_hook,
        cfg_update_hook,
        cfg_mint_hook,
        cfg_burn_hook,
        config
    )
    if asset_id == 0 {
        // --- Config update ---
        plumb.verify_auth(cfg_admin)
    } else {
        // --- Metadata update ---
        let leaf_asset_id: Field = divine()
        let leaf_owner_id: Field = divine()
        let leaf_nonce: Field = divine()
        let leaf_auth_hash: Field = divine()
        let leaf_lock_until: Field = divine()
        let leaf_collection_id: Field = divine()
        let leaf_metadata_hash: Field = divine()
        let leaf_royalty_bps: Field = divine()
        let leaf_creator_id: Field = divine()
        let leaf_flags: Field = divine()
        // Authenticate leaf against old state root
        let old_leaf: Digest = hash_leaf(
            leaf_asset_id,
            leaf_owner_id,
            leaf_nonce,
            leaf_auth_hash,
            leaf_lock_until,
            leaf_collection_id,
            leaf_metadata_hash,
            leaf_royalty_bps,
            leaf_creator_id,
            leaf_flags
        )
        let leaf_idx: U32 = as_u32(divine())
        merkle.verify(old_leaf, old_root, leaf_idx, plumb.tree_depth())
        assert_eq(leaf_asset_id, asset_id)
        // Owner authorization
        plumb.verify_auth(leaf_auth_hash)
        // Flag check: must be updatable
        assert_updatable(leaf_flags)
        // New leaf: only metadata_hash and nonce change
        let new_nonce: Field = leaf_nonce + 1
        let new_leaf: Digest = hash_leaf(
            leaf_asset_id,
            leaf_owner_id,
            new_nonce,
            leaf_auth_hash,
            leaf_lock_until,
            leaf_collection_id,
            new_metadata_hash,
            leaf_royalty_bps,
            leaf_creator_id,
            leaf_flags
        )
        // Authenticate new leaf against new state root
        merkle.verify(new_leaf, new_root, leaf_idx, plumb.tree_depth())
        reveal
        MetadataUpdate { asset_id: leaf_asset_id, old_metadata: leaf_metadata_hash, new_metadata: new_metadata_hash }
    }
    // Hook signal
    plumb.signal_hook(cfg_update_hook)
}

// ============================================================
// Op 3: MINT โ€” originate a new unique asset
// ============================================================
fn mint() {
    let old_root: Digest = pub_read5()
    let new_root: Digest = pub_read5()
    let old_count: Field = pub_read()
    let new_count: Field = pub_read()
    let max_supply: Field = pub_read()
    let asset_id: Field = pub_read()
    let metadata_hash: Field = pub_read()
    let collection_id: Field = pub_read()
    let config: Digest = pub_read5()
    // --- Verify config ---
    let cfg_admin: Field = divine()
    let cfg_pay: Field = divine()
    let cfg_lock: Field = divine()
    let cfg_mint: Field = divine()
    let cfg_burn: Field = divine()
    let cfg_pay_hook: Field = divine()
    let cfg_lock_hook: Field = divine()
    let cfg_update_hook: Field = divine()
    let cfg_mint_hook: Field = divine()
    let cfg_burn_hook: Field = divine()
    plumb.verify_config(
        cfg_admin,
        cfg_pay,
        cfg_lock,
        cfg_mint,
        cfg_burn,
        cfg_pay_hook,
        cfg_lock_hook,
        cfg_update_hook,
        cfg_mint_hook,
        cfg_burn_hook,
        config
    )
    // Mint authorization (always required, 0 = minting disabled)
    plumb.verify_auth(cfg_mint)
    // Supply accounting
    let expected_count: Field = old_count + 1
    assert_eq(new_count, expected_count)
    // Max supply enforcement (0 = unlimited)
    if max_supply == 0 {
    } else {
        let headroom: Field = sub(max_supply, new_count)
        plumb.assert_non_negative(headroom)
    }
    // Asset fields from prover
    let owner_id: Field = divine()
    let auth_hash: Field = divine()
    let creator_id: Field = divine()
    let royalty_bps: Field = divine()
    let flags: Field = divine()
    // Validate fields
    plumb.assert_non_negative(royalty_bps)
    let royalty_headroom: Field = sub(10000, royalty_bps)
    plumb.assert_non_negative(royalty_headroom)
    assert_valid_flags(flags)
    // Flag check: must be mintable (collection open for mints)
    assert_mintable(flags)
    // New leaf: nonce = 0, lock_until = 0
    let new_leaf: Digest = hash_leaf(
        asset_id,
        owner_id,
        0,
        auth_hash,
        0,
        collection_id,
        metadata_hash,
        royalty_bps,
        creator_id,
        flags
    )
    // Authenticate new leaf against new state root
    let leaf_idx: U32 = as_u32(divine())
    merkle.verify(new_leaf, new_root, leaf_idx, plumb.tree_depth())
    // Hook signal
    plumb.signal_hook(cfg_mint_hook)
    // Events
    reveal
    Mint { asset_id: asset_id, creator_id: creator_id, collection_id: collection_id, metadata_hash: metadata_hash }
    reveal
    SupplyChange { old_count: old_count, new_count: new_count }
}

// ============================================================
// Op 4: BURN โ€” permanently destroy an asset
// ============================================================
fn burn() {
    let old_root: Digest = pub_read5()
    let new_root: Digest = pub_read5()
    let old_count: Field = pub_read()
    let new_count: Field = pub_read()
    let asset_id: Field = pub_read()
    let current_time: Field = pub_read()
    let config: Digest = pub_read5()
    // --- Verify config ---
    let cfg_admin: Field = divine()
    let cfg_pay: Field = divine()
    let cfg_lock: Field = divine()
    let cfg_mint: Field = divine()
    let cfg_burn: Field = divine()
    let cfg_pay_hook: Field = divine()
    let cfg_lock_hook: Field = divine()
    let cfg_update_hook: Field = divine()
    let cfg_mint_hook: Field = divine()
    let cfg_burn_hook: Field = divine()
    plumb.verify_config(
        cfg_admin,
        cfg_pay,
        cfg_lock,
        cfg_mint,
        cfg_burn,
        cfg_pay_hook,
        cfg_lock_hook,
        cfg_update_hook,
        cfg_mint_hook,
        cfg_burn_hook,
        config
    )
    // --- Asset leaf ---
    let leaf_asset_id: Field = divine()
    let leaf_owner_id: Field = divine()
    let leaf_nonce: Field = divine()
    let leaf_auth_hash: Field = divine()
    let leaf_lock_until: Field = divine()
    let leaf_collection_id: Field = divine()
    let leaf_metadata_hash: Field = divine()
    let leaf_royalty_bps: Field = divine()
    let leaf_creator_id: Field = divine()
    let leaf_flags: Field = divine()
    // Authenticate leaf against old state root
    let old_leaf: Digest = hash_leaf(
        leaf_asset_id,
        leaf_owner_id,
        leaf_nonce,
        leaf_auth_hash,
        leaf_lock_until,
        leaf_collection_id,
        leaf_metadata_hash,
        leaf_royalty_bps,
        leaf_creator_id,
        leaf_flags
    )
    let leaf_idx: U32 = as_u32(divine())
    merkle.verify(old_leaf, old_root, leaf_idx, plumb.tree_depth())
    assert_eq(leaf_asset_id, asset_id)
    // Owner authorization
    plumb.verify_auth(leaf_auth_hash)
    // Dual auth if burn_auth โ‰  0
    if cfg_burn == 0 {
    } else {
        plumb.verify_auth(cfg_burn)
    }
    // Time-lock check
    let lock_headroom: Field = sub(current_time, leaf_lock_until)
    plumb.assert_non_negative(lock_headroom)
    // Flag check: must be burnable
    assert_burnable(leaf_flags)
    // Supply accounting
    let expected_count: Field = sub(old_count, 1)
    assert_eq(new_count, expected_count)
    // Hook signal
    plumb.signal_hook(cfg_burn_hook)
    // Nullifier (sealed โ€” prevents double-burn)
    seal Nullifier { asset_id: leaf_asset_id, nonce: leaf_nonce }
    // Events
    reveal
    Burn { asset_id: leaf_asset_id, owner_id: leaf_owner_id }
    reveal
    SupplyChange { old_count: old_count, new_count: new_count }
}

// ============================================================
// Entry point โ€” TSP-2 dispatch by PLUMB operation code
// ============================================================
fn main() {
    let op: Field = pub_read()
    if op == 0 {
        pay()
    } else if op == 1 {
        lock()
    } else if op == 2 {
        update()
    } else if op == 3 {
        mint()
    } else if op == 4 {
        burn()
    }
}

Local Graph