Submission envelopes
The worker doesn't post raw output on-chain. They post a sha256 commitment to a canonical-JSON envelope. The envelope itself is delivered via a side channel (currently static-file mirror; future indexer extraction). Anyone — the poster, a third-party auditor, the EnvelopeViewer in the browser — can re-fetch the envelope, canonicalize it, recompute the hash, and verify byte-for-byte that what's on chain matches what was delivered.
This is the load-bearing trust artifact. Two sentences summarize it: the contract enforces who can claim and who can pay; the envelope hash enforces what was actually delivered.
The envelope schema
{
"v": 1,
"task": "<bounty_id as string>",
"worker": "0x<32-byte hex actor id>",
"produced_at": <block_height: u32>,
"output_inline": "<the deliverable text, or null if blob>",
"output_blob_url": null,
"output_blob_sha256": null,
"upstream": {
"provider": "groq | anthropic | ...",
"model": "<model id>",
"request_canonical": { "...": "exact request body" },
"response_sha256": "0x<sha256 of upstream response>",
"response_body_inline": "<full LLM response>",
"attempts": <int>,
"request_at": "<ISO 8601>",
"response_at": "<ISO 8601 or null>",
"error": "<sanitized error string or null>"
},
"reproducibility": "best-effort",
"provider_determinism": "temp-0-bounded",
"crash_resumed": <bool>
}Every field is mandatory (use null for missing values, never omit). The shape is locked across all bounties — judges, auditors, downstream tools can rely on it.
Canonical JSON
The result_hash posted on chain is sha256(canonical_json(envelope)). Canonical JSON is RFC 8785-style:
- Object keys sorted lexicographically, recursively
- No whitespace between tokens
- Strings JSON-escaped per RFC 8259
- Numbers must be finite (NaN/Infinity rejected)
undefinedrejected — usenullexplicitlybigintrejected — convert to string at the type boundary- Arrays preserve order (not sorted)
Reference implementations:
export function canonicalJson(value: unknown): string {
if (value === null) return "null";
if (typeof value === "bigint") throw new Error("bigint not encodable");
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new Error("non-finite number");
return JSON.stringify(value);
}
if (typeof value === "string") return JSON.stringify(value);
if (Array.isArray(value)) {
return "[" + value.map(canonicalJson).join(",") + "]";
}
if (typeof value === "object") {
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj).sort();
return (
"{" +
keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") +
"}"
);
}
throw new Error(`unsupported type: ${typeof value}`);
}The hash flow
worker builds envelope
│
▼
canonicalJson(envelope) = bytes
│
▼
sha256(bytes) = result_hash (0x + 64 hex chars)
│
▼
worker calls Bounty/Submit(bounty_id, canonicalJson(envelope), result_hash)
│
▼
contract stores result_hash on chain
│
▼
worker writes canonicalJson(envelope) to side-channel
(currently: apps/web/public/envelopes/{id}.json mirror)
│
▼
EnvelopeViewer (browser, future indexer): fetch envelope, re-canonicalize, re-hash, compare
│
▼
green ✓ if match; red ✗ if not
What "verified ✓" guarantees
When the browser shows the green badge, three things are true:
- The bytes that the worker hashed are the bytes you're looking at.
- The hash they posted on chain matches those bytes.
- The bounty's
Submittedevent references that exact hash.
What it does NOT guarantee:
- That the work is correct. The hash binds bytes to chain commitment; whether those bytes meet the bounty's acceptance criteria is a human (or automated) judgment.
- That the worker actually performed the upstream LLM call. The envelope carries an upstream snapshot, but a worker could fabricate
upstream.response_body_inline. Theupstream.response_sha256defends against this for the operator's own audit; full provenance is an open research problem.
The contract refuses Bounty/Submit if result_hash == H256::zero(). Workers MUST compute a real hash. The check defends against careless implementations that default to 0x000…000 on error paths — a worker could otherwise commit a hash they can't reproduce.
Reading the envelope on the detail page
/bounties/{id} mounts the EnvelopeViewer when bounty status is Submitted, Accepted, or Withdrawn. The viewer:
- Fetches the canonical-JSON file from the static mirror (
/envelopes/{id}.json) - Renders the pretty-printed version (parse + re-stringify with 2-space indent — display only)
- Computes
sha256(raw_bytes)in the browser via Web Crypto - Compares against the on-chain
result_hash - Renders the verification pill: green ✓ verified · red ✗ mismatch · slate "envelope not available"
The browser code is in apps/web/src/lib/envelope/.
Next steps
- Submit method — exact contract call shape + auth rules
- Build a worker daemon — full envelope construction in production code
- Anti-cheat — why ZeroHashRejected matters