Repository navigation
On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21
Description
Context
ContextMEM has never signed a Sui tx (everything is Tatum/MemWal-mediated or read-only via SuiGrpcClient in packages/walrus/src/proof.ts), and a tool call leaves no verifiable, on-chain trace of who ran which capability over what. The roadmap §2/§6 wants each invocation of the triad to (a) write a usage/event-ledger row and (b) mint an on-chain Receipt (op, namespace, producer, source_digest, artifact_digest, walrus_blob_id, parents lineage) composing into a Nexus DAG. extract already produces chunkGraphDigest() and packProofBundle().artifactDigest; remember produces a WalrusStorageReceipt with blobId/jobId — the exact fields a receipt needs. This must degrade gracefully to ledger-only until the Move receipt module is published. (Consolidates the runtime PTB client + the per-tool-call receipt hook.)
Goal / user story
As the extract/remember/recall pipeline, after a Walrus upload certifies I can register/update the namespace and mint an attribution receipt on-chain via a single signed-and-(Enoki-)sponsored PTB, and a hook emits a typed receipt (ledger now, on-chain when enabled) on EVERY triad tool call whose id is returned in the tool output — so an agent's DAG run is provably anchored on Sui and downstream nodes chain via parents.
Acceptance criteria
- New runtime-agnostic module
packages/walrus/src/onchain.ts(nonode:fs/child_process, importable from the Worker — mirrorproof.ts, NOT the Node-onlystorage.ts):mintReceipt(input)builds aTransaction(@mysten/sui/transactions) callingcontextmem::receipt::mint_receiptwithop,namespace, digests (hex→vector<u8>),walrus_blob_id, optionalquilt_patch_id/memwal_ref/model, andparents: ID[]; returns the minted receipt object id + tx digest. -
ensureNamespace(input)callsregister_namespaceidempotently (skip if already registered) and, on mint, advancesset_head_receipt; signing loads the service key viadecodeSuiPrivateKey→Ed25519Keypair, gated by env (CONTEXTMEM_SUI_PRIVATE_KEY,CONTEXTMEM_MOVE_PACKAGE_ID,CONTEXTMEM_SUI_NETWORK) added toWorkerEnv(apps/api/src/worker.ts:44) +.env.example, resolved likeresolveMemwalCreds(worker.ts:3341). - Enoki gas sponsorship: build → sponsor → sign → execute as one tight sequence (sponsor sigs expire fast); fall back to the service key's own testnet balance if Enoki is unconfigured. Minting is best-effort — failures are logged, never break the run (like
captureWalrusSiteProof). - A
withReceipt(op, handler)wrapper inpackages/mcpwraps each triad handler; every triad tool output envelope gainsreceipt: { id, mode: 'ledger'|'onchain', ... }, andrecall/rememberaccept optionalparents: string[]. - A pluggable
ReceiptSinkhas a ledger sink (writes tocontextmem_usage_eventsvia the Worker, or local JSONL for Node CLI/MCP) and an on-chain sink (guarded byCONTEXTMEM_RECEIPTS=onchain) that callsonchain.ts; receipt struct field names match the Movereceiptmodule (op enum Extract/Recall/Remember, source_digest, artifact_digest, walrus_blob_id, parents, created_at) so ledger rows backfill cleanly on-chain. - A mapping helper converts a
WalrusStorageReceipt+StorageIndexInput(packages/memwal/src/index.ts:37) into mint args; wired (behind a feature flag) into the upload+remember call sites in MCP (packages/mcp/src/index.ts:200-222) and CLI (packages/cli/src/index.ts:347); dedupe by namespace+artifactDigestso re-runs don't mint duplicates, and persist the returned receipt id (extendrememberStorageIndex/storage.json). - Tests: the PTB targets the right
package::module::functionand encodes digests as bytes; receipt emitted on success;parentsthreaded through; sink/mint failure does not break the tool.
Implementation notes
- Worker compatibility is the gotcha.
@mysten/suiTransaction-build + Ed25519 sign + grpc execute should run in the Worker (the read-only grpc client already does inproof.ts), but verify bundle/CPU budget. This is the easy crypto path — do NOT couple this to the risky@mysten/sealWorker spike. - Digest encoding: strip the
sha256:prefix fromartifactDigestand hex-decode to bytes; usechunkGraphDigest(chunks)output forsource_digest. Forrecall,source_digest= the queried namespace head. - Where it runs: preferred trigger is after Walrus certify in the remember flow. From the Worker run inline or, to dodge sponsor-expiry under load, off a Cloudflare Queue consumer — keep
onchain.tspure so either host works. Reuse the credential-resolution pattern frommemwal/index.tsfor the producer identity. - Coordinate the struct with the Move
receiptissue; confirm Nexus's exact receipt schema/event ABI before freezing field names. Enoki sponsorship plumbing overlaps Harbor's reserve→finalize — reuse the sameEd25519Keypair/sponsorship code if Harbor lands first.
Sui Overflow angle
This is the moment ContextMEM signs its first real Sui transaction — every agent tool call produces a verifiable Sui attribution receipt, turning ContextMEM into a Nexus-composable, provenance-anchored memory layer. A judge can follow a DAG run extract → remember → recall as linked receipt objects on a Sui explorer — concrete proof of Sui-native agent infrastructure, not just an API.
Dependencies
Requires the Move scaffold (published packageId) + the receipt module (and registry for ensureNamespace/set_head_receipt) + the canonical MCP triad (handlers to wrap). Ships ledger-only first (over the usage-event ledger).
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
Add a runtime-agnostic
packages/walrus/src/onchain.ts(mirrorsproof.ts— pure@mysten/sui, nonode:fs/child_process) that builds and signs the first real ContextMEM Sui txs (receipt::mint_receipt/registry::register_namespace/set_head_receipt), plus a pluggableReceiptSink+withReceipt(op, handler)inpackages/mcpso every triad tool call emits a typed receipt. Key decision: ship ledger-only first (the on-chain sink is a no-op untilCONTEXTMEM_MOVE_PACKAGE_IDis set), receipt struct field names are frozen to the Movereceiptmodule so ledger rows backfill cleanly on-chain, and minting is strictly best-effort (mirrorscaptureWalrusSiteProof— failures logged, never break the run). This is deliberately the easy crypto path (tx-build + Ed25519 sign + execute, already proven viable sinceproof.ts'sSuiGrpcClientruns in the Worker) and must NOT be coupled to the risky@mysten/sealWorker spike (#5).Prerequisites & dependencies
receiptmodule (mint_receipt) +registrymodule (register_namespace,set_head_receipt) with a knownpackageId. NoMove.tomlexists in the repo today. Until then the on-chain sink is dormant and only the ledger sink runs.extract/recall/remember) thatwithReceiptwraps. They do not exist yet — current MCP tools areremember_site_snapshot(mcp:161),recall_site_context(mcp:234),upload_proof_to_walrus(mcp:188). WirewithReceiptonto those for now and re-point when the triad lands.contextmem_usage_eventsD1 table + a Worker write endpoint) — roadmap §6 P0. It is NOT built (migrations stop at0006_namespace_build_sources.sql). The ledger sink writes there via the Worker; for Node CLI/MCP it falls back to local JSONL, so this issue can land without it.resolveMemwalCredsat worker.ts:3341):CONTEXTMEM_SUI_PRIVATE_KEY(asuiprivkey1…service key with a funded Sui testnet balance for the non-Enoki fallback),CONTEXTMEM_MOVE_PACKAGE_ID,CONTEXTMEM_SUI_NETWORK(=testnet),CONTEXTMEM_RECEIPTS(ledger|onchain), optionalENOKI_API_KEY+CONTEXTMEM_REGISTRY_ID.@mysten/enokiis already innode_modules.walrus_blob_idwould cross networks — pick one), Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5 signer (Enoki-sponsored vs relayer vs user wallet) + the exact Nexus receipt schema/event ABI, Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6 service-key custody (shared vs per-owner), Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9 D1-vs-chain source of truth.Step-by-step
"@mysten/sui": "^2.9.1"(and"@mysten/enoki") as explicit deps inpackages/walrus/package.json(today it relies on root hoisting). ExtendWorkerEnv(apps/api/src/worker.ts:32-55) withCONTEXTMEM_SUI_PRIVATE_KEY,CONTEXTMEM_MOVE_PACKAGE_ID,CONTEXTMEM_SUI_NETWORK,CONTEXTMEM_RECEIPTS,ENOKI_API_KEY?,CONTEXTMEM_REGISTRY_ID?; mirror them in.env.example. Add a smallresolveOnchainConfig(env)(next toresolveMemwalCreds, worker.ts:3341) that returns{ privateKey, packageId, network, registryId?, enokiApiKey? }from env.packages/walrus/src/onchain.ts(mirrorproof.ts; export it frompackages/walrus/src/index.tslike the other modules). Imports:Transactionfrom@mysten/sui/transactions,Ed25519Keypairfrom@mysten/sui/keypairs/ed25519,decodeSuiPrivateKeyfrom@mysten/sui/cryptography,SuiClientfrom@mysten/sui/client(JSON-RPC-over-fetch for execute) and/orSuiGrpcClientfrom@mysten/sui/grpc(reads, consistent with proof.ts). Define:type ReceiptOp = 'Extract' | 'Recall' | 'Remember'.type MintReceiptInput = { op, namespace, sourceDigest, artifactDigest, walrusBlobId?, quiltPatchId?, memwalRef?, model?, parents: string[] }.type OnchainConfig = { privateKey, packageId, network, registryId?, enokiApiKey? }.mintReceipt(input, config): build aTransaction,tx.moveCall({ target: \${config.packageId}::receipt::mint_receipt`, arguments: [ op-as-enum/u8, tx.pure.string(namespace), tx.pure.vector('u8', hexToBytes(sourceDigest)), tx.pure.vector('u8', hexToBytes(artifactDigest)), tx.pure.string(walrusBlobId ?? ''), …optionals, tx.makeMoveVec({ type:'ID', elements: parents.map(p=>tx.pure.id(p)) }) ] }). Sign+(sponsor)+execute, parseobjectChanges/effects.createdfor the new receipt object id, return{ id, txDigest, mode: 'onchain' }`.ensureNamespace(input, config): read the registry for an existing namespace object; if absent callregistry::register_namespace; after a mint, callregistry::set_head_receipt. Idempotent (skip register if found).hexToBytes(d): strip a leadingsha256:then hex-decode toUint8Array(artifactDigest issha256:<hex>per storage.ts:243;chunkGraphDigestis bare hex per chunks.ts:59).sponsorAndExecute(tx, keypair, config): ifenokiApiKeyset → build kind bytes → Enoki create-sponsored → sign → Enoki execute, as one tight sequence (sponsor sigs expire fast); elsekeypair.signAndExecuteTransactionagainst the service key's own testnet gas. Whole thing wrapped so any throw is caught upstream.onchain.ts):receiptInputFromStorage(receipt: WalrusStorageReceipt, ctx: { namespace, target, runId, op, sourceDigest?, parents? }): MintReceiptInput— mapsreceipt.blobId→walrusBlobId,receipt.artifactDigest→artifactDigest,ctx.sourceDigest(fromchunkGraphDigest(chunks))→sourceDigest. Accept a minimal structuralctx(not the fullStorageIndexInput) to avoid awalrus→memwalcircular import.packages/mcp/src/receipts.ts— define the typedReceipt({ id, mode: 'ledger'|'onchain', op, namespace, producer, source_digest, artifact_digest, walrus_blob_id, parents, created_at, txDigest? }, field names matching the Move struct), aReceiptSinkinterface,LedgerSink(POST to the Worker usage-event endpoint, or append JSONL toCONTEXTMEM_RECEIPTS_DIRfor Node),OnchainSink(callsensureNamespace+mintReceipt, guarded byCONTEXTMEM_RECEIPTS==='onchain'), andselectSink(env).withReceipt(op, handler)inpackages/mcp(e.g.receipts.ts): wraps a tool handler, runs it, derives mint args from its result, callssink.emit(...)best-effort (swallow errors), and foldsreceipt: { id, mode, … }into the output envelope. Wrap the current triad-equivalent handlers —remember_site_snapshot(mcp:161),recall_site_context(mcp:234),upload_proof_to_walrus(mcp:188) — and add optionalparents: z.array(z.string()).optional()to therecall/remembertool schemas, threading it into the receipt.CONTEXTMEM_RECEIPTSflag):upload_proof_to_walrus(packages/mcp/src/index.ts:197-223): afteruploadProofBundle+rememberStorageIndex, buildreceiptInputFromStorage(opRemember,sourceDigestfrom chunk graph if available), dedupe bynamespace + artifactDigest(checkstorage.json/ local ledger), emit via the sink, persist the returned receipt id intostorage.json(written at mcp:208).storage push(packages/cli/src/index.ts:341-378, esp. receipt write at :359): same flow.rememberStorageIndex/renderStorageIndex(packages/memwal/src/index.ts:164+:281) andStorageIndexInput(:37) to carry/persistreceiptId.wrangler secret/README note for the new env vars.New / changed files
packages/walrus/src/onchain.ts(new)packages/walrus/src/index.ts(export onchain)packages/walrus/package.json(add@mysten/sui,@mysten/enoki)packages/mcp/src/receipts.ts(new —ReceiptSink,withReceipt)packages/mcp/src/index.ts(wrap triad handlers; addparents; wire mint intoupload_proof_to_walrus~:197-223)packages/cli/src/index.ts(wire mint intostorage push~:341-378)packages/memwal/src/index.ts(StorageIndexInput+rememberStorageIndex/renderStorageIndexgainreceiptId)apps/api/src/worker.ts(WorkerEnv+resolveOnchainConfig).env.example(new Sui/Enoki/receipts vars)packages/walrus/test/onchain.test.ts(new),packages/mcp/test/receipts.test.ts(new)Test plan
packageId; inspecttx.getData()and assert themoveCalltarget is exactly${pkg}::receipt::mint_receipt, that the two digest args areUint8Arrayhex-decoded with thesha256:prefix stripped (length 32), andparentsare encoded as a vector ofID. AsserthexToByteson bothsha256:…and bare-hex inputs.parentsis threaded into the emitted receipt; a sink/mint that throws is swallowed and the tool output is unchanged (best-effort contract).receiptInputFromStoragemapsblobId/artifactDigest/sourceDigestcorrectly and dedupes onnamespace+artifactDigest.packageIdlands): runcontextmem storage push --rememberwithCONTEXTMEM_RECEIPTS=onchainagainst a published testnet package; assert it returns a real Sui testnet tx digest + receipt object id, and that a follow-onrecallwithparents=[<receiptId>]produces a child receipt whoseparentslink the first — provable as linked objects on a Sui explorer (the demo round-tripextract → remember → recall).storage.jsoncarries the persistedreceiptId.Risks & gotchas
proof.ts), but verify the compressed-bundle cap after adding@mysten/sui/transactions. If it busts, run minting off the Cloudflare Queue consumer —onchain.tsis pure so either host works. Do not entangle with the@mysten/sealspike (Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5).walrus_blob_idcrosses networks. Pin the demo to one network (roadmap decision docs: add product roadmap (Harbor storage + Talus on-chain direction) #4) before relying on the link.CONTEXTMEM_MOVE_PACKAGE_IDunset the on-chain sink is dormant; everything degrades to ledger-only (this is the intended first ship).suiprivkey1…(bech32, viadecodeSuiPrivateKey) — distinct from the raw 64-hex MemWal delegate; don't reuseisMemwalSeed/sanitizeMemwalKey.source_digest/artifact_digest/walrus_blob_id/parents/created_at) before freezing, so ledger rows backfill cleanly.contextmem_usage_eventsisn't shipped yet, the Worker LedgerSink has no target — keep the JSONL fallback for Node and make the Worker sink degrade gracefully.Acceptance-criteria mapping
onchain.tswithmintReceipt(digests hex→vector<u8>, parentsID[], returns object id + tx digest) → Step 2.ensureNamespaceidempotentregister_namespace+set_head_receipt; service key viadecodeSuiPrivateKey→Ed25519Keypair; env gating added toWorkerEnv/.env.exampleresolved likeresolveMemwalCreds→ Steps 1 + 2.sponsorAndExecute).withReceipt(op, handler)inpackages/mcp; envelope gainsreceipt:{id,mode,…};recall/rememberacceptparents→ Steps 4-5.ReceiptSink: ledger (usage_events via Worker / JSONL) + on-chain (CONTEXTMEM_RECEIPTS=onchain) callingonchain.ts; field names match Move struct → Step 4.WalrusStorageReceipt+StorageIndexInput→mint args; wired (flagged) into MCP (mcp:200-222) + CLI (cli:347); dedupe by namespace+artifactDigest; persist receipt id → Steps 3 + 6.Effort
L — ~4-5 person-days for the deliverable-now ledger path +
onchain.tsscaffolding + sink/wrapper + call-site wiring + unit tests; +1-2 person-days to validate a real on-chain mint (a testnet tx digest + DAG round-trip) once the MovepackageIdlands.Implementation plan generated for execution (planning phase). Part of the roadmap (#4).