Repository navigation
Move module registry: on-chain namespace ownership object linking to Walrus blob + Harbor/SEAL refs #20
Description
Context
Namespace ownership, visibility, and access are 100% off-chain in D1 today — CloudflareNamespaceStore in apps/api/src/worker.ts stores owner_id + visibility + ctxm_* read tokens, and docs/namespace-model.md confirms "private" is just a Bearer-token check, not anything on-chain. This module makes namespace ownership a real Sui object so it can be the authority record for provenance (roadmap §2) and the link target between a namespace and its Walrus blob / Harbor Bucket / SEAL policy.
Goal / user story
As a namespace owner, my namespace exists as an on-chain Namespace object that records who owns it, whether it's public/private, its SEAL policy (when private), and a pointer to its latest attribution receipt — so ownership and provenance are verifiable on Sui, not just trusted from a D1 row.
Acceptance criteria
-
module contextmem::registrydefines aNamespacestruct with:name: String(thedemo:/web:/walrus:namespace string fromdocs/namespace-model.md),owner: address,visibility: u8(0=public,1=private),seal_policy_id: Option<ID>,display_name: String,description: String,walrus_blob_id: Option<String>,created_at_ms: u64,head_receipt: Option<ID>. -
public fun register_namespace(...)creates a sharedNamespace(so the sponsored service signer and later readers can both touch it) and emits aNamespaceRegisteredevent. - Owner-gated mutators (assert
ns.owner == ctx.sender()or an explicit cap):set_visibility,set_seal_policy,set_display,set_walrus_blob,transfer_ownership. -
public fun set_head_receipt(ns: &mut Namespace, receipt_id: ID, ctx)— owner/authority gated — lets the receipt-mint flow advance the head pointer (consumed by thereceiptmodule / on-chain TS client). -
#[test]coverage: register, owner-only mutation succeeds, non-owner mutation aborts, visibility/seal-policy round-trip. - Getters for all fields for off-chain reads.
Implementation notes
- Source-of-truth decision (roadmap open question §2): for the alpha, keep D1 authoritative and the on-chain
Namespaceas an attestation/mirror — far less plumbing than making chain authoritative. Document this; the TS client backfills/syncs D1 → chain. - Visibility ↔ SEAL:
visibility=1(private) should carry aseal_policy_idpopulated from Harbor's on-chain Seal policy object (private namespace ⇔ owned Seal policy, per roadmap §1). Confirm Harbor exposes the policy id to store here.walrus_blob_idmirrors theWalrusStorageReceipt.blobId. - Authority model: with the scaffold's signer decision (service key signs, Enoki sponsors gas),
ns.ownerwill initially be the service key address. If/when user wallets land,transfer_ownershiphands it to the user. Avoid baking user-wallet assumptions in now. - Cross-module head pointer:
set_head_receiptis the seam thereceiptmint calls; keep the auth check here soreceiptstays a pure attestation minter. A dedicatedWriterCap(roadmap §2 P2capmodule) can replace thesender-equality check later — out of scope. - Shared vs owned: prefer shared
Namespaceso a sponsored backend tx can mutatehead_receiptwithout the object being in the user's address; document the consensus-cost tradeoff.
Sui Overflow angle
Turns "namespace ownership" from an invisible database column into a first-class Sui object that links Walrus storage + SEAL access control + attribution receipts into one verifiable graph — the on-chain backbone judges can inspect, and the natural home for the private-namespace SEAL story.
Dependencies
- Requires the Move scaffold issue.
- Pairs with the
receiptmodule (itshead_receipt/parentsreference these objects). - SEAL policy id linkage depends on Harbor integration landing — the field can ship now and be populated later.
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
Author one new Move module,
contextmem::registry, in the package stood up by the scaffold issue (#8) — this replaces the emptymove/contextmem/sources/registry.movestub. It defines a sharedNamespaceobject that mirrors today's D1contextmem_namespacesrow (owner/visibility/display/description + new on-chain pointersseal_policy_id,walrus_blob_id,head_receipt), withregister_namespace+ owner-gated mutators + aset_head_receiptseam for the receipt-mint flow. Key design decisions: (1) D1 stays authoritative, the on-chain object is an attestation/mirror (per issue note + roadmap §2 open-Q #9) — so this module deliberately does NOT enforce on-chain name uniqueness; idempotency is the TS client's job (#21). (2)head_receiptis stored as an opaqueOption<ID>soregistrynever importsreceipt— avoiding a circular module dependency while still letting a PTB pass a freshly-minted receipt id. (3) Shared (not owned) so an Enoki-sponsored backend tx signed by the service key can advancehead_receiptwithout the object living in a user address.Prerequisites & dependencies
move/contextmem/withMove.toml(edition2024.beta,contextmem = "0x0", Sui framework pinned to a testnet rev), thesources/registry.movestub this issue fills in, thetests/dir, and.github/workflows/move.yml(build+test on PRs touchingmove/**, plus theworkflow_dispatchpublish→published.testnet.jsonpackageId). Do not author CI here.receiptmodule).Namespace.head_receipt: Option<ID>holds areceipt::Receiptobject id. No code dependency (opaqueID), but the struct field shapes for Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9 + Move module registry: on-chain namespace ownership object linking to Walrus blob + Harbor/SEAL refs #20 should be frozen together before the first testnet publish (a field change = re-publish = new packageId = breaking for On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21).packages/walrus/src/onchain.ts).ensureNamespacecallsregister_namespaceidempotently and the mint flow callsset_head_receipt; that issue owns the D1→chain backfill/sync.seal_policy_idships now, populated later when Harbor exposes the policy id. No blocking dep — the field isOption<ID>.SUI_TESTNET_PRIVATE_KEYrepo secret + faucet gas already wired by Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8. The alpha authority model = the service-key address isns.owner(service signs, Enoki sponsors); do not bake user-wallet assumptions in.Step-by-step
move/contextmem/Move.toml,move/contextmem/sources/registry.move(stubmodule contextmem::registry {}), andmove/contextmem/tests/exist (from Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8). Runsui move buildto confirm a green baseline before editing.Namespacestruct + constants inmove/contextmem/sources/registry.move. Usestd::string::String,sui::object::{Self, ID, UID},std::option::{Self, Option},sui::clock::Clock,sui::event,sui::tx_context::TxContext. Struct (has key, store):id: UID,name: String,owner: address,visibility: u8,seal_policy_id: Option<ID>,display_name: String,description: String,walrus_blob_id: Option<String>,created_at_ms: u64,head_receipt: Option<ID>.const VISIBILITY_PUBLIC: u8 = 0; const VISIBILITY_PRIVATE: u8 = 1;and abort codesconst ENotOwner: u64 = 0; const EBadVisibility: u64 = 1;.namemaps to thedemo:/web:/walrus:/target:string fromnamespaceForTarget(packages/core/src/utils.ts:123) /docs/namespace-model.md;walrus_blob_idmirrorsWalrusStorageReceipt.blobId(packages/core/src/types.ts:655);visibility/owner/display_name/descriptionmirror the D1 columns read inCloudflareNamespaceStore(apps/api/src/worker.ts:401, SELECT at:415).public fun register_namespace(name, owner, visibility, display_name, description, clock: &Clock, ctx: &mut TxContext): assertvisibility∈ {0,1} (EBadVisibility);let ns = Namespace { id: object::new(ctx), name, owner, visibility, seal_policy_id: option::none(), display_name, description, walrus_blob_id: option::none(), created_at_ms: clock.timestamp_ms(), head_receipt: option::none() }; captureobject::uid_to_inner(&ns.id)for the event;event::emit(NamespaceRegistered { id, name: ns.name, owner, visibility });transfer::share_object(ns). Definepublic struct NamespaceRegistered has copy, drop { id: ID, name: String, owner: address, visibility: u8 }. (Takeowneras a param rather thanctx.sender()so the TS client can register on behalf of the service key explicitly.)assert!(ns.owner == ctx.sender(), ENotOwner)first, allpublic fun ... (ns: &mut Namespace, ..., ctx: &TxContext):set_visibility(ns, visibility: u8, ctx)— also assertvisibilityvalid.set_seal_policy(ns, seal_policy_id: ID, ctx)—ns.seal_policy_id = option::some(seal_policy_id)(and aclear_seal_policyfor the public path, optional).set_display(ns, display_name: String, description: String, ctx).set_walrus_blob(ns, blob_id: String, ctx)—option::some(...).transfer_ownership(ns, new_owner: address, ctx)— setns.owner = new_owner(reserved for when user wallets land).fun assert_owner(ns: &Namespace, ctx: &TxContext).public fun set_head_receipt(ns: &mut Namespace, receipt_id: ID, ctx: &TxContext)— owner-gated viaassert_owner;ns.head_receipt = option::some(receipt_id). This is the seam the mint PTB (On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21) calls afterreceipt::mint_receiptreturns the new id; keep the auth check here soreceiptstays a pure attestation minter (aWriterCapcan replace the sender check later — roadmap §2 P2, out of scope). Document that this requiresmint_receiptto return theID(coordinate with Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9) so a single PTB can mint then advance the head.devInspect):public fun name(&Namespace): String,owner(): address,visibility(): u8,seal_policy_id(): Option<ID>,display_name(): String,description(): String,walrus_blob_id(): Option<String>,created_at_ms(): u64,head_receipt(): Option<ID>. (String/ID/Optionof those all havecopy, so return-by-value is fine.)move/contextmem/tests/registry_tests.moveusingsui::test_scenario+sui::clock::create_for_testing:register→ take the shared object, assert getters equal seeded values; assertNamespaceRegisteredemitted (test_scenario::take_shared+ check viatest_scenario::next_txevent count / returned effects).set_visibility/set_seal_policy/set_display/set_walrus_blob/set_head_receipt/transfer_ownership, assert via getters (visibility + seal-policy round-trip explicitly).#[test, expected_failure(abort_code = contextmem::registry::ENotOwner)]calling a mutator from a second address.EBadVisibilitypath:#[expected_failure]onregister_namespace/set_visibilitywithvisibility = 2.move/contextmem/README.md(extend Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8's): document the D1-authoritative / chain-mirror decision, the shared-vs-owned consensus-cost tradeoff, and the alphaowner == service-key-addressmodel.sui move build && sui move testlocally; PR triggersmove.yml(Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8). Optionally trigger theworkflow_dispatchpublish to refreshpublished.testnet.jsonso On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21 has the packageId (coordinate so Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9 lands in the same publish).New / changed files
move/contextmem/sources/registry.move(fill in the Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8 stub — the module).move/contextmem/tests/registry_tests.move(new).move/contextmem/README.md(append source-of-truth + shared-object + authority notes).Test plan
sui move testcovers register, owner mutation success, non-owner abort (expected_failureonENotOwner), bad-visibility abort, and visibility/seal-policy/head-receipt round-trip via getters — runs inmove.ymlon everymove/**PR.workflow_dispatchpublishes to testnet (Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8), run a realsui client call --package <pkgId> --module registry --function register_namespace ...(Clock at0x6) — produces a concrete testnet tx digest + a sharedNamespaceobject id viewable on suiscan/suiexplorer, withNamespaceRegisteredin the tx events. Verify getters viasui client ptb/ devInspect. (Full TS register→mutate→read round-trip is On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21's test surface.)set_visibilitycall fails on-chain (paste the abort), proving owner-gating is real, not a D1 token check.Risks & gotchas
CONTEXTMEM_MOVE_PACKAGE_ID. Freeze Move module registry: on-chain namespace ownership object linking to Walrus blob + Harbor/SEAL refs #20 + Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9 field shapes together before publishing.use contextmem::receiptforhead_receipt; store the opaqueOption<ID>. (Receipt → registry direction is also avoided:set_head_receiptis called by the TS PTB, not byreceipt.)head_receipt/visibility without the object being in the user's address — document the tradeoff.register_namespacecalls with the samenamecreate two objects. Acceptable because D1 is authoritative and On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21 dedupes; aRegistrytable object for uniqueness is explicitly out of scope.ctx.sender()is thesuiprivkey1...service address (Enoki sponsors gas).transfer_ownershipexists for the future user-wallet path but is not exercised yet.walrus_blob_idmay reference a Tatum mainnet blob — note this cross-network caveat in the README; reconcile at launch (roadmap §1).public struct ... has copy, drop; pullcreated_at_msfrom&Clock(param0x6); useclock::create_for_testing(ctx)in tests.Acceptance-criteria mapping
Namespacestruct with all 9 fields → Step 2.register_namespacecreates shared object + emitsNamespaceRegistered→ Step 3.set_visibility/set_seal_policy/set_display/set_walrus_blob/transfer_ownership→ Step 4.set_head_receipt(ns, receipt_id, ctx)→ Step 5.#[test]coverage (register, owner-only success, non-owner abort, visibility/seal-policy round-trip) → Step 7.Effort
M — ~1.5–2 person-days. The module is small and fully specified (~150–220 lines incl. tests), but budget time for Sui
test_scenario/Clocktest ergonomics, gettingsui move testgreen, freezing struct shapes jointly with #9, and a testnet publish dry-run. Hard-blocked on #8 landing first.Implementation plan generated for execution (planning phase). Part of the roadmap (#4).