Skip to content

On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21

Open

Description

@harrymove-ctrl

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 (no node:fs/child_process, importable from the Worker — mirror proof.ts, NOT the Node-only storage.ts): mintReceipt(input) builds a Transaction (@mysten/sui/transactions) calling contextmem::receipt::mint_receipt with op, namespace, digests (hex→vector<u8>), walrus_blob_id, optional quilt_patch_id/memwal_ref/model, and parents: ID[]; returns the minted receipt object id + tx digest.
  • ensureNamespace(input) calls register_namespace idempotently (skip if already registered) and, on mint, advances set_head_receipt; signing loads the service key via decodeSuiPrivateKey → Ed25519Keypair, gated by env (CONTEXTMEM_SUI_PRIVATE_KEY, CONTEXTMEM_MOVE_PACKAGE_ID, CONTEXTMEM_SUI_NETWORK) added to WorkerEnv (apps/api/src/worker.ts:44) + .env.example, resolved like resolveMemwalCreds (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 in packages/mcp wraps each triad handler; every triad tool output envelope gains receipt: { id, mode: 'ledger'|'onchain', ... }, and recall/remember accept optional parents: string[].
  • A pluggable ReceiptSink has a ledger sink (writes to contextmem_usage_events via the Worker, or local JSONL for Node CLI/MCP) and an on-chain sink (guarded by CONTEXTMEM_RECEIPTS=onchain) that calls onchain.ts; receipt struct field names match the Move receipt module (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+artifactDigest so re-runs don't mint duplicates, and persist the returned receipt id (extend rememberStorageIndex / storage.json).
  • Tests: the PTB targets the right package::module::function and encodes digests as bytes; receipt emitted on success; parents threaded through; sink/mint failure does not break the tool.

Implementation notes

  • Worker compatibility is the gotcha. @mysten/sui Transaction-build + Ed25519 sign + grpc execute should run in the Worker (the read-only grpc client already does in proof.ts), but verify bundle/CPU budget. This is the easy crypto path — do NOT couple this to the risky @mysten/seal Worker spike.
  • Digest encoding: strip the sha256: prefix from artifactDigest and hex-decode to bytes; use chunkGraphDigest(chunks) output for source_digest. For recall, 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.ts pure so either host works. Reuse the credential-resolution pattern from memwal/index.ts for the producer identity.
  • Coordinate the struct with the Move receipt issue; confirm Nexus's exact receipt schema/event ABI before freezing field names. Enoki sponsorship plumbing overlaps Harbor's reserve→finalize — reuse the same Ed25519Keypair/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

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P1Important: hardens the demo and core product
    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
    Add a runtime-agnostic packages/walrus/src/onchain.ts (mirrors proof.ts — pure @mysten/sui, no node:fs/child_process) that builds and signs the first real ContextMEM Sui txs (receipt::mint_receipt / registry::register_namespace / set_head_receipt), plus a pluggable ReceiptSink + withReceipt(op, handler) in packages/mcp so every triad tool call emits a typed receipt. Key decision: ship ledger-only first (the on-chain sink is a no-op until CONTEXTMEM_MOVE_PACKAGE_ID is set), receipt struct field names are frozen to the Move receipt module so ledger rows backfill cleanly on-chain, and minting is strictly best-effort (mirrors captureWalrusSiteProof — failures logged, never break the run). This is deliberately the easy crypto path (tx-build + Ed25519 sign + execute, already proven viable since proof.ts's SuiGrpcClient runs in the Worker) and must NOT be coupled to the risky @mysten/seal Worker spike (#5).

    Prerequisites & dependencies

    • Blocked-by (for the on-chain path only): the Move scaffold must be published — receipt module (mint_receipt) + registry module (register_namespace, set_head_receipt) with a known packageId. No Move.toml exists in the repo today. Until then the on-chain sink is dormant and only the ledger sink runs.
    • Blocked-by: the canonical MCP triad handlers (extract/recall/remember) that withReceipt wraps. They do not exist yet — current MCP tools are remember_site_snapshot (mcp:161), recall_site_context (mcp:234), upload_proof_to_walrus (mcp:188). Wire withReceipt onto those for now and re-point when the triad lands.
    • Depends-on: the usage/event ledger (contextmem_usage_events D1 table + a Worker write endpoint) — roadmap §6 P0. It is NOT built (migrations stop at 0006_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.
    • External creds/bindings (new env, gated like resolveMemwalCreds at worker.ts:3341): CONTEXTMEM_SUI_PRIVATE_KEY (a suiprivkey1… 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), optional ENOKI_API_KEY + CONTEXTMEM_REGISTRY_ID. @mysten/enoki is already in node_modules.
    • Open roadmap decisions to confirm before freezing the struct: docs: add product roadmap (Harbor storage + Talus on-chain direction) #4 network for the launch surface (Tatum blobs are mainnet, the Move package is testnet → a receipt citing a walrus_blob_id would 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

    1. Deps + env plumbing. Add "@mysten/sui": "^2.9.1" (and "@mysten/enoki") as explicit deps in packages/walrus/package.json (today it relies on root hoisting). Extend WorkerEnv (apps/api/src/worker.ts:32-55) with CONTEXTMEM_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 small resolveOnchainConfig(env) (next to resolveMemwalCreds, worker.ts:3341) that returns { privateKey, packageId, network, registryId?, enokiApiKey? } from env.
    2. Create packages/walrus/src/onchain.ts (mirror proof.ts; export it from packages/walrus/src/index.ts like the other modules). Imports: Transaction from @mysten/sui/transactions, Ed25519Keypair from @mysten/sui/keypairs/ed25519, decodeSuiPrivateKey from @mysten/sui/cryptography, SuiClient from @mysten/sui/client (JSON-RPC-over-fetch for execute) and/or SuiGrpcClient from @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 a Transaction, 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, parse objectChanges/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 call registry::register_namespace; after a mint, call registry::set_head_receipt. Idempotent (skip register if found).
      • hexToBytes(d): strip a leading sha256: then hex-decode to Uint8Array (artifactDigest is sha256:<hex> per storage.ts:243; chunkGraphDigest is bare hex per chunks.ts:59).
      • sponsorAndExecute(tx, keypair, config): if enokiApiKey set → build kind bytes → Enoki create-sponsored → sign → Enoki execute, as one tight sequence (sponsor sigs expire fast); else keypair.signAndExecuteTransaction against the service key's own testnet gas. Whole thing wrapped so any throw is caught upstream.
    3. Mapping helper (in onchain.ts): receiptInputFromStorage(receipt: WalrusStorageReceipt, ctx: { namespace, target, runId, op, sourceDigest?, parents? }): MintReceiptInput — maps receipt.blobId→walrusBlobId, receipt.artifactDigest→artifactDigest, ctx.sourceDigest (from chunkGraphDigest(chunks))→sourceDigest. Accept a minimal structural ctx (not the full StorageIndexInput) to avoid a walrus→memwal circular import.
    4. packages/mcp/src/receipts.ts — define the typed Receipt ({ id, mode: 'ledger'|'onchain', op, namespace, producer, source_digest, artifact_digest, walrus_blob_id, parents, created_at, txDigest? }, field names matching the Move struct), a ReceiptSink interface, LedgerSink (POST to the Worker usage-event endpoint, or append JSONL to CONTEXTMEM_RECEIPTS_DIR for Node), OnchainSink (calls ensureNamespace+mintReceipt, guarded by CONTEXTMEM_RECEIPTS==='onchain'), and selectSink(env).
    5. withReceipt(op, handler) in packages/mcp (e.g. receipts.ts): wraps a tool handler, runs it, derives mint args from its result, calls sink.emit(...) best-effort (swallow errors), and folds receipt: { 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 optional parents: z.array(z.string()).optional() to the recall/remember tool schemas, threading it into the receipt.
    6. Wire the upload+remember call sites (behind the CONTEXTMEM_RECEIPTS flag):
      • MCP upload_proof_to_walrus (packages/mcp/src/index.ts:197-223): after uploadProofBundle + rememberStorageIndex, build receiptInputFromStorage (op Remember, sourceDigest from chunk graph if available), dedupe by namespace + artifactDigest (check storage.json / local ledger), emit via the sink, persist the returned receipt id into storage.json (written at mcp:208).
      • CLI storage push (packages/cli/src/index.ts:341-378, esp. receipt write at :359): same flow.
      • Extend rememberStorageIndex / renderStorageIndex (packages/memwal/src/index.ts:164 + :281) and StorageIndexInput (:37) to carry/persist receiptId.
    7. Tests (see below) and a 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; add parents; wire mint into upload_proof_to_walrus ~:197-223)
    • packages/cli/src/index.ts (wire mint into storage push ~:341-378)
    • packages/memwal/src/index.ts (StorageIndexInput + rememberStorageIndex/renderStorageIndex gain receiptId)
    • 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

    • Unit (onchain.ts): build a tx with a fake packageId; inspect tx.getData() and assert the moveCall target is exactly ${pkg}::receipt::mint_receipt, that the two digest args are Uint8Array hex-decoded with the sha256: prefix stripped (length 32), and parents are encoded as a vector of ID. Assert hexToBytes on both sha256:… and bare-hex inputs.
    • Unit (receipts/withReceipt): receipt is emitted on success and folded into the envelope; parents is threaded into the emitted receipt; a sink/mint that throws is swallowed and the tool output is unchanged (best-effort contract).
    • Unit (mapping): receiptInputFromStorage maps blobId/artifactDigest/sourceDigest correctly and dedupes on namespace+artifactDigest.
    • Integration / manual (gated, once packageId lands): run contextmem storage push --remember with CONTEXTMEM_RECEIPTS=onchain against a published testnet package; assert it returns a real Sui testnet tx digest + receipt object id, and that a follow-on recall with parents=[<receiptId>] produces a child receipt whose parents link the first — provable as linked objects on a Sui explorer (the demo round-trip extract → remember → recall).
    • Round-trip assert: re-running the same run does NOT mint a duplicate (dedupe holds) and storage.json carries the persisted receiptId.

    Risks & gotchas

    • Worker bundle/CPU: tx-build + Ed25519 sign + execute should fit (grpc reads already run in the Worker via proof.ts), but verify the compressed-bundle cap after adding @mysten/sui/transactions. If it busts, run minting off the Cloudflare Queue consumer — onchain.ts is pure so either host works. Do not entangle with the @mysten/seal spike (Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5).
    • Sponsor-sig expiry: Enoki sponsor sigs expire fast → build→sponsor→sign→execute must be one tight sequence; under load, move to the Queue consumer.
    • Network split: Tatum blobs are mainnet, the Move package is testnet — a receipt citing walrus_blob_id crosses 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.
    • No package yet: with CONTEXTMEM_MOVE_PACKAGE_ID unset the on-chain sink is dormant; everything degrades to ledger-only (this is the intended first ship).
    • Key format: the service key is suiprivkey1… (bech32, via decodeSuiPrivateKey) — distinct from the raw 64-hex MemWal delegate; don't reuse isMemwalSeed/sanitizeMemwalKey.
    • Field-name freeze / ABI: confirm the Nexus receipt schema + the Move struct (op enum order, source_digest/artifact_digest/walrus_blob_id/parents/created_at) before freezing, so ledger rows backfill cleanly.
    • Ledger endpoint: if contextmem_usage_events isn'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

    • Runtime-agnostic onchain.ts with mintReceipt (digests hex→vector<u8>, parents ID[], returns object id + tx digest) → Step 2.
    • ensureNamespace idempotent register_namespace + set_head_receipt; service key via decodeSuiPrivateKey→Ed25519Keypair; env gating added to WorkerEnv/.env.example resolved like resolveMemwalCreds → Steps 1 + 2.
    • Enoki build→sponsor→sign→execute, fallback to own balance, best-effort → Step 2 (sponsorAndExecute).
    • withReceipt(op, handler) in packages/mcp; envelope gains receipt:{id,mode,…}; recall/remember accept parents → Steps 4-5.
    • Pluggable ReceiptSink: ledger (usage_events via Worker / JSONL) + on-chain (CONTEXTMEM_RECEIPTS=onchain) calling onchain.ts; field names match Move struct → Step 4.
    • Mapping helper WalrusStorageReceipt+StorageIndexInput→mint args; wired (flagged) into MCP (mcp:200-222) + CLI (cli:347); dedupe by namespace+artifactDigest; persist receipt id → Steps 3 + 6.
    • Tests: PTB target + byte-encoded digests; receipt on success; parents threaded; sink/mint failure non-fatal → Test plan.

    Effort
    L — ~4-5 person-days for the deliverable-now ledger path + onchain.ts scaffolding + 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 Move packageId lands.


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

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

    P1Important: hardens the demo and core productagent-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