Repository navigation
Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9
Description
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::receiptdefines aReceiptstruct 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, setscreated_at_msfrom a&ClockandepochfromTxContext, then freezes it (transfer::freeze_object) so the attestation is immutable + globally queryable by ID. -
mint_receiptemits aReceiptMintedevent 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 aparent_count/getter for read access. -
#[test]coverage: mint with 0 parents and with N parents, assert event emission and that fields round-trip (usesui::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.
artifactDigestflows in as the string"sha256:<hex>"(seeWalrusStorageReceipt.artifactDigest,packages/core/src/types.ts:664); the TS client should strip thesha256:prefix and pass raw 32-bytevector<u8>. Same forsource_digest(usechunkGraphDigest's hex output). - Freeze vs share: freeze receipts (immutable) — they're attestations, never mutated. A frozen object's
IDis still referenceable fromNamespace.head_receiptand from child receipts'parents. - Head pointer lives in
registry: updatingNamespace.head_receiptis a separate mutable-object operation; expose a public entry inregistryrather 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
Receiptper 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_receiptupdate path depends on theregistrymodule issue (can land independently; mint works without it).
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
Author a single Move module
contextmem::receiptthat mints an immutable (frozen), globally-queryableReceiptobject per agent op, plus a thin TypeScript mapping layer inpackages/walrus(PTB builder + typed reader + digest helper) so the Worker/CLI/Nexus can mint and read receipts without BCS-decoding. Key design decisions: (1)produceris an explicit parameter (nottx_context::sender) so an Enoki-sponsored/relayer tx still records the real agent identity, not the sponsor; (2) receipts arefreeze_object'd (attestations, never mutated) — thehead_receiptmutable pointer is deliberately left to theregistrymodule; (3) digests are stored as rawvector<u8>(32-byte sha256), with the TS client stripping thesha256:prefix. This is PLANNING ONLY.Prerequisites & dependencies
Move.tomland zero.movefiles in the repo (confirmed). That issue must create the package skeleton (e.g.move/contextmem/Move.tomlwith the Sui framework dep + named addresscontextmem), addsui move build && sui move testto CI, and ownsui client publishto testnet. Thisreceiptmodule drops itssources/receipt.moveinto that scaffold.registrymodule issue: ownsNamespace.head_receiptupdates.mint_receiptworks standalone without it (per acceptance criteria: parents compose the DAG; head pointer is a separate mutable op).@mysten/sui@^2.9.1is a root dep and already used inpackages/walrus(bcs.ts,proof.tsuse@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
Create the Move module at
move/contextmem/sources/receipt.move(path under the scaffolded package). Modulecontextmem::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}.Define the
Receiptstruct (has key, store) with exactly the acceptance-criteria fields, in this order (1:1 withStorageIndexInput/renderStorageIndexnaming — seepackages/memwal/src/index.ts:37andrenderStorageIndexat:281which emitstarget/namespace/artifactDigest/walrusBlobId/status):id: UIDop: u8— document the enum in a doc comment: 0=Extract, 1=Recall, 2=Remembernamespace: String,producer: address,tool_caller: Option<address>source_digest: vector<u8>(fromchunkGraphDigest,packages/core/src/chunks.ts:59),artifact_digest: vector<u8>(fromWalrusStorageReceipt.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: u64Add
const OP_EXTRACT: u8 = 0; const OP_RECALL: u8 = 1; const OP_REMEMBER: u8 = 2;and anEInvalidOp/EBadDigestLenerror const.Define the
ReceiptMintedevent struct (has copy, drop) carryingreceipt_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).Implement
public entry fun mint_receipt(...)taking every field as a param plusclock: &Clockandctx: &mut TxContext. Body:assert!(op <= OP_REMEMBER, EInvalidOp);and optionallyassert!(vector::length(&source_digest) == 32 && vector::length(&artifact_digest) == 32, EBadDigestLen);(sha256).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
entryso it's callable directly in a PTB / sponsored tx.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, pluspublic fun id(r: &Receipt): IDandpublic fun parent_count(r: &Receipt): u64 { vector::length(&r.parents) }(criteria docs: add product roadmap (Harbor storage + Talus on-chain direction) #4).Write Move unit tests at
move/contextmem/tests/receipt_tests.move(criteria Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5) usingsui::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);callmint_receiptwithvector::empty<ID>();let effects = ts::next_tx(&mut scenario, @0xA);asserttest_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 avector<ID>of N synthetic IDs (mint N receipts first, collect their ids, or useobject::id_from_address); assertparent_count == Nandparentsround-trip verbatim.Add the TS mapping layer at
packages/walrus/src/receipt.ts(export frompackages/walrus/src/index.ts):hexDigestToBytes(d: string): Uint8Array— stripssha256:prefix thenfromHex(...)(already imported from@mysten/sui/utilsinbcs.ts). Used for bothartifact_digest(fromWalrusStorageReceipt.artifactDigest) andsource_digest(fromchunkGraphDigest).type AttributionReceipt(TS mirror of the struct) +const OP = { Extract:0, Recall:1, Remember:2 }.buildMintReceiptTx(args, pkgId): Transaction— constructs a@mysten/sui/transactionsPTB calling${pkgId}::receipt::mint_receiptwith 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— readsgetObjectcontent.fields(Sui returns parsed Move fields) into the typed shape so Nexus/TS clients never BCS-decode.(Scaffold-owned, call out in PR): add
sui move build/sui move testto CI (bun-check.ymlis TS-only today) and aCONTEXTMEM_PACKAGE_IDconstant/env consumed byreceipt.tsafter publish.New / changed files
move/contextmem/sources/receipt.move(new) — the modulemove/contextmem/tests/receipt_tests.move(new) — unit testspackages/walrus/src/receipt.ts(new) — digest helper, PTB builder, typed reader,AttributionReceipttype +OPenumpackages/walrus/src/index.ts(edit) — re-exportreceipt.tsmove/contextmem/Move.toml(edit, owned by scaffold issue) — ensure framework dep +contextmemnamed address.github/workflows/bun-check.yml(edit, coordinate w/ scaffold issue) — addsui move testTest plan
sui move testruns 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.receipt.test.ts(vitest, matchesquilt.test.tspattern in the package) assertinghexDigestToBytes("sha256:"+("ab".repeat(32)))yields a 32-byteUint8Array, andparseReceiptObjectmaps a fixture Sui object →AttributionReceipt(op/namespace/digests/parents). Runs underbun run check.sui client publishto testnet, then executebuildMintReceiptTxonce with real values (op=Remember, a realwalrusBlobIdfrom a Tatum receipt,artifact_digestfrompackProofBundle,source_digestfromchunkGraphDigest, 1 parent). Capture the real testnet tx digest; thengetObject(receiptId, {showContent:true})and assert the frozen object's fields equal the inputs and the object isImmutable-owned. Also verify theReceiptMintedevent appears in tx effects. Paste the explorer link + digest in the PR as proof.parents=[extractId], confirmparent_count==1andparents[0]resolves to the extract receipt.Risks & gotchas
String/vectorfields can't be both emitted in the event and moved into the struct without cloning — clone (copy) the fields needed byReceiptMintedbefore constructingReceipt, or build the event from copies. Watch this in review.registryop) — never a mutation. Document this.length==0 || length==32, or make the digest fields tolerate empty. Recall ops have no artifact_digest.tx_context::senderis the sponsor — that's exactly whyproduceris a param. Don't "simplify" it tosender(ctx).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.Acceptance-criteria mapping
mint_receiptsetscreated_at_msfrom&Clock,epochfromTxContext, thentransfer::freeze_object→ Step 4.ReceiptMintedwith 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 viatest_scenario+clock::create_for_testing→ Step 6.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).
hegu-1 commentedon Jun 27, 2026
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:
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.