Skip to content

Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9

Open

Description

@harrymove-ctrl

Context

The one thing no existing primitive (Walrus bytes, Harbor/SEAL access control, MemWal recall) provides is a typed, queryable, composable attribution receipt that a Talus Nexus DAG node can reference by object ref and verify (roadmap §2). Today provenance is weak: packProofBundle computes a sha256 artifactDigest (packages/walrus/src/storage.ts:243) and chunkGraphDigest a deterministic content digest (packages/core/src/chunks.ts:59), but neither is anchored to an identity-bound on-chain object — and Tatum's key, not ours, signs the blob. This module is the Talus payload that fixes that.

Goal / user story

As an agent builder using the contextmem.extract/.recall/.remember Talus Tools, each tool run mints a verifiable on-chain Receipt object that records what operation happened, over what source/artifact, by whom, with which model, and which prior receipts it descends from — so I can audit and compose agent memory provenance as a DAG.

Acceptance criteria

  • module contextmem::receipt defines a Receipt struct with: op: u8 (0=Extract,1=Recall,2=Remember; document the enum), namespace: String, producer: address, tool_caller: Option<address>, source_digest: vector<u8>, artifact_digest: vector<u8>, walrus_blob_id: String, quilt_patch_id: Option<String>, memwal_ref: Option<String>, parents: vector<ID>, model: Option<String>, created_at_ms: u64, epoch: u64.
  • public fun mint_receipt(...) constructs the object, sets created_at_ms from a &Clock and epoch from TxContext, then freezes it (transfer::freeze_object) so the attestation is immutable + globally queryable by ID.
  • mint_receipt emits a ReceiptMinted event carrying the new receipt id, op, namespace, producer, and digests (so off-chain indexers can follow the DAG without loading every object).
  • parents: vector<ID> is accepted and stored verbatim so receipts compose into a lineage DAG (recall→remember→extract); add a parent_count/getter for read access.
  • #[test] coverage: mint with 0 parents and with N parents, assert event emission and that fields round-trip (use sui::test_scenario + clock::create_for_testing).
  • Public getters for every field so the TS client / Nexus can read receipts without BCS-decoding raw object bytes.

Implementation notes

  • Digests are bytes, not strings. artifactDigest flows in as the string "sha256:<hex>" (see WalrusStorageReceipt.artifactDigest, packages/core/src/types.ts:664); the TS client should strip the sha256: prefix and pass raw 32-byte vector<u8>. Same for source_digest (use chunkGraphDigest's hex output).
  • Freeze vs share: freeze receipts (immutable) — they're attestations, never mutated. A frozen object's ID is still referenceable from Namespace.head_receipt and from child receipts' parents.
  • Head pointer lives in registry: updating Namespace.head_receipt is a separate mutable-object operation; expose a public entry in registry rather than mutating the namespace from this module.
  • Schema freeze risk: confirm the Talus Nexus receipt schema/event ABI before finalizing struct field order/types (roadmap open question §2). Keep field naming aligned with StorageIndexInput/rememberStorageIndex (packages/memwal/src/index.ts:37,164) so the TS mapping is 1:1.
  • Granularity: start with one Receipt per op (precise lineage). If gas becomes a concern, a later issue can add a batched/Merkle-root variant — out of scope here.

Sui Overflow angle

This is the centerpiece that makes ContextMEM's agent-memory product genuinely Sui-native: composable, on-chain, model-tagged provenance for an AI agent DAG is a novel use of Move objects + events, and demos beautifully ("every fact your agent remembered has a verifiable on-chain receipt with full lineage"). It's the differentiator over plain Walrus storage.

Dependencies

  • Requires the Move scaffold issue (package + CI + publish).
  • The head_receipt update path depends on the registry module issue (can land independently; mint works without it).

Part of the ContextMEM roadmap (#4) • Sui Overflow build.

Activity

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P0Demo-blocking: required for a working Sui Overflow demo
    moveSui Move smart-contract package (registry/receipt)
    suiSui chain: tx signing, objects, wallet, zkLogin, explorer
    agent-talusTalus/Nexus agent integration and attribution receipts
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    Author a single Move module contextmem::receipt that mints an immutable (frozen), globally-queryable Receipt object per agent op, plus a thin TypeScript mapping layer in packages/walrus (PTB builder + typed reader + digest helper) so the Worker/CLI/Nexus can mint and read receipts without BCS-decoding. Key design decisions: (1) producer is an explicit parameter (not tx_context::sender) so an Enoki-sponsored/relayer tx still records the real agent identity, not the sponsor; (2) receipts are freeze_object'd (attestations, never mutated) — the head_receipt mutable pointer is deliberately left to the registry module; (3) digests are stored as raw vector<u8> (32-byte sha256), with the TS client stripping the sha256: prefix. This is PLANNING ONLY.

    Prerequisites & dependencies

    • BLOCKING — Move scaffold issue (the "package + CI + publish" prereq this issue cites under Dependencies): there is currently no Move.toml and zero .move files in the repo (confirmed). That issue must create the package skeleton (e.g. move/contextmem/Move.toml with the Sui framework dep + named address contextmem), add sui move build && sui move test to CI, and own sui client publish to testnet. This receipt module drops its sources/receipt.move into that scaffold.
    • NON-blocking — registry module issue: owns Namespace.head_receipt updates. mint_receipt works standalone without it (per acceptance criteria: parents compose the DAG; head pointer is a separate mutable op).
    • Open roadmap decision (schema freeze) — must resolve before merge: confirm the Talus Nexus receipt schema / event ABI (roadmap §2 open question; "Get the spec before freezing the struct"). Field order/types below are the proposal; lock them against Nexus first.
    • Network: publish to Sui testnet (Move + Harbor live on testnet; Tatum blobs are mainnet — note the split, roadmap decision docs: add product roadmap (Harbor storage + Talus on-chain direction) #4). Needs a testnet keypair with gas (faucet) to publish + run the live mint verification, OR Enoki sponsorship (deferred to the wiring issue).
    • Already present: @mysten/sui@^2.9.1 is a root dep and already used in packages/walrus (bcs.ts, proof.ts use @mysten/sui/bcs, /grpc, /utils) — no new TS dependency needed for the client layer. Sui CLI (sui move) needed locally/CI for the Move side.

    Step-by-step

    1. Create the Move module at move/contextmem/sources/receipt.move (path under the scaffolded package). Module contextmem::receipt. Imports: sui::object::{Self, UID, ID}, sui::tx_context::{Self, TxContext}, sui::clock::{Self, Clock}, sui::transfer, sui::event, std::string::String, std::option::{Self, Option}.

    2. Define the Receipt struct (has key, store) with exactly the acceptance-criteria fields, in this order (1:1 with StorageIndexInput/renderStorageIndex naming — see packages/memwal/src/index.ts:37 and renderStorageIndex at :281 which emits target/namespace/artifactDigest/walrusBlobId/status):

      • id: UID
      • op: u8 — document the enum in a doc comment: 0=Extract, 1=Recall, 2=Remember
      • namespace: String, producer: address, tool_caller: Option<address>
      • source_digest: vector<u8> (from chunkGraphDigest, packages/core/src/chunks.ts:59), artifact_digest: vector<u8> (from WalrusStorageReceipt.artifactDigest, packages/core/src/types.ts:664)
      • walrus_blob_id: String, quilt_patch_id: Option<String>, memwal_ref: Option<String>
      • parents: vector<ID>, model: Option<String>, created_at_ms: u64, epoch: u64
        Add const OP_EXTRACT: u8 = 0; const OP_RECALL: u8 = 1; const OP_REMEMBER: u8 = 2; and an EInvalidOp/EBadDigestLen error const.
    3. Define the ReceiptMinted event struct (has copy, drop) carrying receipt_id: ID, op: u8, namespace: String, producer: address, source_digest: vector<u8>, artifact_digest: vector<u8> (criteria fix(ci): force bun + pin wrangler in Deploy workflow #3: indexers follow the DAG without loading every object).

    4. Implement public entry fun mint_receipt(...) taking every field as a param plus clock: &Clock and ctx: &mut TxContext. Body:

      • assert!(op <= OP_REMEMBER, EInvalidOp); and optionally assert!(vector::length(&source_digest) == 32 && vector::length(&artifact_digest) == 32, EBadDigestLen); (sha256).
      • Build let receipt = Receipt { id: object::new(ctx), ..., created_at_ms: clock::timestamp_ms(clock), epoch: tx_context::epoch(ctx) };
      • let rid = object::uid_to_inner(&receipt.id);
      • event::emit(ReceiptMinted { receipt_id: rid, op, namespace, producer, source_digest, artifact_digest }); (clone the String/vectors needed for the event before moving them into the struct, or emit using copies)
      • transfer::freeze_object(receipt); (criteria Memory knowledge graph: 3D neural constellation + grounded entity panel #2: immutable + globally queryable by ID).
        Make it entry so it's callable directly in a PTB / sponsored tx.
    5. Add public getters for every field (criteria Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6): public fun op(r: &Receipt): u8, namespace, producer, tool_caller, source_digest, artifact_digest, walrus_blob_id, quilt_patch_id, memwal_ref, parents (returns &vector<ID> or a copy), model, created_at_ms, epoch, plus public fun id(r: &Receipt): ID and public fun parent_count(r: &Receipt): u64 { vector::length(&r.parents) } (criteria docs: add product roadmap (Harbor storage + Talus on-chain direction) #4).

    6. Write Move unit tests at move/contextmem/tests/receipt_tests.move (criteria Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5) using sui::test_scenario + sui::clock::create_for_testing:

      • test_mint_zero_parents: ts::begin(@0xA); let clock = clock::create_for_testing(ctx); clock::set_for_testing(&mut clock, 1700000000000); call mint_receipt with vector::empty<ID>(); let effects = ts::next_tx(&mut scenario, @0xA); assert test_scenario::num_user_events(&effects) == 1; ts::take_immutable<Receipt> and assert getters round-trip (op, namespace, digests, created_at_ms == 1700000000000, parent_count == 0); ts::return_immutable; clock::destroy_for_testing(clock); ts::end(scenario);.
      • test_mint_n_parents: pass a vector<ID> of N synthetic IDs (mint N receipts first, collect their ids, or use object::id_from_address); assert parent_count == N and parents round-trip verbatim.
    7. Add the TS mapping layer at packages/walrus/src/receipt.ts (export from packages/walrus/src/index.ts):

      • hexDigestToBytes(d: string): Uint8Array — strips sha256: prefix then fromHex(...) (already imported from @mysten/sui/utils in bcs.ts). Used for both artifact_digest (from WalrusStorageReceipt.artifactDigest) and source_digest (from chunkGraphDigest).
      • type AttributionReceipt (TS mirror of the struct) + const OP = { Extract:0, Recall:1, Remember:2 }.
      • buildMintReceiptTx(args, pkgId): Transaction — constructs a @mysten/sui/transactions PTB calling ${pkgId}::receipt::mint_receipt with the right pure/arg encodings (tx.pure.u8, tx.pure.string, tx.pure.option, tx.pure.vector('u8', bytes), tx.pure.vector('id', parents), tx.object(SUI_CLOCK_OBJECT_ID)).
      • parseReceiptObject(obj): AttributionReceipt — reads getObject content.fields (Sui returns parsed Move fields) into the typed shape so Nexus/TS clients never BCS-decode.
      • Out of scope here: actually signing/sponsoring the tx (Enoki vs relayer — roadmap open question Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5) lives in the "wire receipt minting into extract/remember" issue. This issue ships the module, the PTB builder, the reader, and tests.
    8. (Scaffold-owned, call out in PR): add sui move build/sui move test to CI (bun-check.yml is TS-only today) and a CONTEXTMEM_PACKAGE_ID constant/env consumed by receipt.ts after publish.

    New / changed files

    • move/contextmem/sources/receipt.move (new) — the module
    • move/contextmem/tests/receipt_tests.move (new) — unit tests
    • packages/walrus/src/receipt.ts (new) — digest helper, PTB builder, typed reader, AttributionReceipt type + OP enum
    • packages/walrus/src/index.ts (edit) — re-export receipt.ts
    • move/contextmem/Move.toml (edit, owned by scaffold issue) — ensure framework dep + contextmem named address
    • .github/workflows/bun-check.yml (edit, coordinate w/ scaffold issue) — add sui move test

    Test plan

    • Unit (Move): sui move test runs the two tests in step 6 — proves event emission (num_user_events == 1), 0-parent and N-parent mint, and full field round-trip via getters + clock::create_for_testing. This satisfies criteria Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5 directly.
    • Unit (TS): a receipt.test.ts (vitest, matches quilt.test.ts pattern in the package) asserting hexDigestToBytes("sha256:"+("ab".repeat(32))) yields a 32-byte Uint8Array, and parseReceiptObject maps a fixture Sui object → AttributionReceipt (op/namespace/digests/parents). Runs under bun run check.
    • Integration / manual (proves it really works): sui client publish to testnet, then execute buildMintReceiptTx once with real values (op=Remember, a real walrusBlobId from a Tatum receipt, artifact_digest from packProofBundle, source_digest from chunkGraphDigest, 1 parent). Capture the real testnet tx digest; then getObject(receiptId, {showContent:true}) and assert the frozen object's fields equal the inputs and the object is Immutable-owned. Also verify the ReceiptMinted event appears in tx effects. Paste the explorer link + digest in the PR as proof.
    • DAG round-trip: mint an Extract receipt, then a Remember receipt with parents=[extractId], confirm parent_count==1 and parents[0] resolves to the extract receipt.

    Risks & gotchas

    • Schema freeze (highest): once published+frozen, struct field order/types are effectively immutable for existing objects and the Nexus ABI. Lock the Nexus schema first; a wrong type means a republish + reindex.
    • Move borrow/ownership: String/vector fields can't be both emitted in the event and moved into the struct without cloning — clone (copy) the fields needed by ReceiptMinted before constructing Receipt, or build the event from copies. Watch this in review.
    • Frozen ≠ updatable: correct for attestations, but means any "head pointer" or correction must be a new receipt (or a registry op) — never a mutation. Document this.
    • Digest length assert could reject legitimate empty digests (e.g. Recall with no artifact). Decide: allow length==0 || length==32, or make the digest fields tolerate empty. Recall ops have no artifact_digest.
    • producer vs sponsor: if minted via Enoki-sponsored tx, tx_context::sender is the sponsor — that's exactly why producer is a param. Don't "simplify" it to sender(ctx).
    • Network split: receipts on testnet reference walrus_blob_ids that today are mainnet (Tatum). For the grant demo, co-locate (roadmap decision docs: add product roadmap (Harbor storage + Talus on-chain direction) #4) or clearly label cross-network refs.
    • Sponsor-sig expiry / sponsorship plumbing: deferred to the minting-wiring issue, but flag that the eventual mint path reuses Harbor's sponsored-tx pattern (sponsor sigs expire fast).
    • Gas: each receipt is one object + one tx (granularity = one Receipt per op, per the issue). Batched/Merkle variant is explicitly out of scope.

    Acceptance-criteria mapping

    • Receipt struct with all listed fields → Step 2.
    • mint_receipt sets created_at_ms from &Clock, epoch from TxContext, then transfer::freeze_object → Step 4.
    • Emit ReceiptMinted with id/op/namespace/producer/digests → Steps 3 + 4.
    • parents: vector<ID> stored verbatim + parent_count/getter → Steps 2, 5; verified Step 6 (test_mint_n_parents).
    • #[test] mint 0 & N parents, assert event + field round-trip via test_scenario + clock::create_for_testing → Step 6.
    • Public getters for every field → Step 5 (read in TS via Step 7's parseReceiptObject).

    Effort: M, ~2–3 person-days — Move module + tests ~1d; TS client (helper + PTB builder + reader + types + vitest) ~0.5–1d; testnet publish + live mint verification ~0.5d. Assumes the Move scaffold/publish prereq has landed; if not, add that issue's effort.


    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

  4. hegu-1 commented on Jun 27, 2026

    @hegu-1

    This receipt model is interesting because it treats memory provenance as a composable object graph, not just metadata attached after the fact.

    One adjacent pattern I’ve been exploring is keeping a human-readable memory substrate outside the agent runtime, then letting receipts / indexes / vector stores reference it rather than become the only source of truth. In my case that substrate is Markdown + git:

    • stable decisions and policies are authored/reviewed by the human
    • the model maintains derived links, summaries, and consistency checks
    • every output can point back to source notes or decisions
    • promotion from observation -> stable memory is explicit, not automatic

    Starter shape here, if useful as a comparison point:
    https://github.com/hegu-1/personal-memory-vault-starter

    The connection I see with ContextMEM: on-chain receipts could attest to memory operations and lineage, while the vault layer remains the inspectable semantic surface humans and agents both understand. That separation may help avoid making the receipt chain carry too much meaning by itself: receipts prove lineage, but the vault preserves authorial context and decision structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Demo-blocking: required for a working Sui Overflow demoagent-talusTalus/Nexus agent integration and attribution receiptsmoveSui Move smart-contract package (registry/receipt)suiSui chain: tx signing, objects, wallet, zkLogin, explorer

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions