Skip to content

Commit a4f0353

Browse files
committed
h
1 parent 2a15831 commit a4f0353

4 files changed

Lines changed: 561 additions & 3 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,33 @@ It exposes four native Actions (`CHECK_GBLIN_TREASURY_HEALTH`, `INVEST_IDLE_USDC
4141

4242
---
4343

44+
## AI Action Receipts — a witnessed transparency log for what your agent did
45+
46+
The problem this attacks is the #1 barrier to AI adoption in 2026: **nobody can
47+
prove, after the fact, exactly what an AI did** (surveys: workers burn 2–4 h/week
48+
verifying AI output; 70% of orgs say they cannot govern their agents). Our answer
49+
is the smallest honest primitive: a public, append-only **RFC 6962 transparency
50+
log** for AI actions. You send **hashes only** (never content); you get back a
51+
portable receipt any third party can verify offline, forever.
52+
53+
```
54+
receipt = canonical payload
55+
+ Ed25519 signature (key: gblin.digital/receipts-log)
56+
+ RFC 6962 inclusion proof (leaf → Merkle root)
57+
+ C2SP signed checkpoint (origin, tree size, root)
58+
```
59+
60+
- Seal (paid, unlimited): `POST https://gblin.digital/api/x402/seal` — $0.01 USDC via x402.
61+
- Seal (demo, 5/day/IP): `POST <worker>/v1/seal-demo` or MCP tool `seal_action_demo`.
62+
- Read free forever: `<worker>/v1/receipt/:index` · `/log` · `/log/checkpoint` · `/log/proof/:index` · human page `/receipt/:index`.
63+
- Daily EAS anchor on Base of the tree root (verifiable on base.easscan.org, schema `0x9f433a96…`, promiseId `keccak256("gblin-receipts-log")`).
64+
- **Offline verifier, zero dependencies:** [`verify-receipt.mjs`](./verify-receipt.mjs)`node verify-receipt.mjs receipt.json`.
65+
66+
A seal proves **existence and time**, independently witnessed (the same C2SP
67+
witness network that cosigns transparency logs — we already witness-exchange
68+
with a third-party log operator). It is **not** a compliance certificate and
69+
**not** an endorsement of the content. Worker: `<worker>` = `https://gblin-mcp.gblin-mcp-worker.workers.dev`.
70+
4471
## Coherence Proof — verify GBLIN keeps its promises
4572

4673
GBLIN pre-registers hash-pinned public promises, then runs an automaton that probes them every 10 minutes and seals each closed day as an [EAS attestation](https://base.easscan.org/schema/view/0x9f433a96467ab75530009970e5aa938ec94d8a49f08f66e7381822d557b448ef) on Base. Reading is free forever; the paid service is being observed — the certifier submits itself to its own instrument first.

verify-receipt.mjs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env node
2+
// verify-receipt.mjs — offline verifier for GBLIN AI Action Receipts.
3+
// Zero dependencies (Node 18+). You do NOT need to trust gblin.digital:
4+
// this script re-derives everything from the receipt JSON itself.
5+
//
6+
// node verify-receipt.mjs receipt.json
7+
// curl -s https://gblin-mcp.gblin-mcp-worker.workers.dev/v1/receipt/0 | node verify-receipt.mjs /dev/stdin
8+
//
9+
// Checks: (1) canonical payload → leaf hash (RFC 6962, 0x00 prefix);
10+
// (2) Ed25519 receipt signature over "gblin-receipt/v1\n"+canonical;
11+
// (3) inclusion proof leaf→root (0x01 prefix);
12+
// (4) checkpoint note signature (C2SP) over origin/size/root by the SAME key;
13+
// (5) key hash inside the verifier_key matches name+alg+pubkey.
14+
// The daily EAS anchor on Base can be checked independently on base.easscan.org
15+
// (schema 0x9f433a96..., promiseId keccak256("gblin-receipts-log")).
16+
17+
import { readFileSync } from "fs";
18+
import { webcrypto as crypto } from "crypto";
19+
20+
const die = (m) => { console.error("FAIL:", m); process.exit(1); };
21+
const ok = (m) => console.log(" ✓", m);
22+
const b64 = (u8) => Buffer.from(u8).toString("base64");
23+
const unb64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
24+
const te = new TextEncoder();
25+
const cat = (...p) => { const n=p.reduce((a,x)=>a+x.length,0); const o=new Uint8Array(n); let i=0; for(const x of p){o.set(x,i);i+=x.length;} return o; };
26+
const sha256 = async (u8) => new Uint8Array(await crypto.subtle.digest("SHA-256", u8));
27+
28+
function canonicalize(v) {
29+
if (v === null || typeof v !== "object") return JSON.stringify(v);
30+
if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
31+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canonicalize(v[k])).join(",") + "}";
32+
}
33+
34+
const file = process.argv[2] || die("usage: node verify-receipt.mjs receipt.json");
35+
const r = JSON.parse(readFileSync(file, "utf8"));
36+
const receipt = r.receipt || r; // accept either the bare receipt or an API wrapper
37+
if (receipt.format !== "gblin-receipt/v1") die("unknown format: " + receipt.format);
38+
39+
// (5) verifier key structure
40+
const m = /^([^+]+)\+([0-9a-f]{8})\+([A-Za-z0-9+/=]+)$/.exec(receipt.verifier_key) || die("bad verifier_key");
41+
const keyRaw = unb64(m[3]);
42+
if (keyRaw[0] !== 0x01) die("verifier_key alg is not Ed25519 note key (0x01)");
43+
const pub = keyRaw.slice(1);
44+
const kh = (await sha256(cat(te.encode(m[1] + "\n"), keyRaw))).slice(0, 4);
45+
if (Buffer.from(kh).toString("hex") !== m[2]) die("verifier_key hash mismatch");
46+
ok(`verifier key: ${m[1]} (${m[2]})`);
47+
48+
// (1) canonical → leaf
49+
const canonical = canonicalize(receipt.payload);
50+
const leaf = await sha256(cat(Uint8Array.of(0x00), te.encode(canonical)));
51+
if (b64(leaf) !== receipt.leaf) die("leaf hash does not match canonical payload");
52+
ok("leaf = SHA256(0x00 || canonical(payload))");
53+
54+
// (2) receipt signature (present on freshly-sealed receipts; /v1/receipt omits it)
55+
const key = await crypto.subtle.importKey("raw", pub, { name: "Ed25519" }, false, ["verify"]);
56+
if (receipt.signature) {
57+
const good = await crypto.subtle.verify({ name: "Ed25519" }, key, unb64(receipt.signature), te.encode("gblin-receipt/v1\n" + canonical));
58+
if (!good) die("receipt signature invalid");
59+
ok("receipt Ed25519 signature valid");
60+
}
61+
62+
// (3) inclusion proof
63+
let h = leaf, idx = receipt.index, size = receipt.tree_size;
64+
let lo = 0, hi = size;
65+
for (const pB64 of receipt.inclusion_proof) {
66+
const sib = unb64(pB64);
67+
let k = 1; while (k * 2 < hi - lo) k *= 2;
68+
if (idx < lo + k) { h = await sha256(cat(Uint8Array.of(0x01), h, sib)); hi = lo + k; }
69+
else { h = await sha256(cat(Uint8Array.of(0x01), sib, h)); lo = lo + k; }
70+
}
71+
// NB: path is leaf→root; recompute bounds properly (redo forward):
72+
{
73+
const proof = receipt.inclusion_proof.map(unb64);
74+
const rec = async (i, a, b, d) => {
75+
if (b - a === 1) return leaf;
76+
let k = 1; while (k * 2 < b - a) k *= 2;
77+
if (i < a + k) { const L = await rec(i, a, a + k, d); return sha256(cat(Uint8Array.of(0x01), L, proof[d.n++] ?? die("proof too short"))); }
78+
const R = await rec(i, a + k, b, d);
79+
return sha256(cat(Uint8Array.of(0x01), proof[d.n++] ?? die("proof too short"), R));
80+
};
81+
// proof array is ordered leaf-upward; rebuild with an index counter walking the same recursion order
82+
const d = { n: 0 };
83+
const order = [];
84+
const collect = (i, a, b) => { if (b - a === 1) return; let k=1; while (k*2 < b-a) k*=2; if (i < a+k) { collect(i,a,a+k); order.push(["R",a+k,b]); } else { collect(i,a+k,b); order.push(["L",a,a+k]); } };
85+
collect(receipt.index, 0, size);
86+
if (order.length !== proof.length) die(`proof length ${proof.length} != expected ${order.length}`);
87+
let cur = leaf;
88+
for (let j = 0; j < order.length; j++) {
89+
cur = order[j][0] === "R" ? await sha256(cat(Uint8Array.of(0x01), cur, proof[j])) : await sha256(cat(Uint8Array.of(0x01), proof[j], cur));
90+
}
91+
if (b64(cur) !== receipt.root) die("inclusion proof does not reach the stated root");
92+
}
93+
ok(`inclusion proof: leaf #${receipt.index} → root of tree size ${size}`);
94+
95+
// (4) checkpoint note
96+
const note = receipt.checkpoint || die("no checkpoint in receipt");
97+
const sep = note.indexOf("\n\n");
98+
const body = note.slice(0, sep + 1);
99+
const lines = body.split("\n");
100+
if (lines[0] !== m[1]) die("checkpoint origin mismatch");
101+
if (Number(lines[1]) !== size) die("checkpoint size != receipt tree_size");
102+
if (lines[2] !== receipt.root) die("checkpoint root != receipt root");
103+
const sigLine = note.slice(sep + 2).split("\n").find((l) => l.startsWith("— " + m[1] + " ")) || die("no signature line for origin");
104+
const payloadSig = unb64(sigLine.split(" ")[2]);
105+
if (payloadSig.length !== 68) die("bad checkpoint sig payload");
106+
if (Buffer.from(payloadSig.slice(0, 4)).toString("hex") !== m[2]) die("checkpoint keyhash mismatch");
107+
const goodCp = await crypto.subtle.verify({ name: "Ed25519" }, key, payloadSig.slice(4), te.encode(body));
108+
if (!goodCp) die("checkpoint signature invalid");
109+
ok("checkpoint (C2SP signed note) valid and consistent with receipt");
110+
111+
console.log(`\nPASS — receipt #${receipt.index} verified offline.`);
112+
console.log(`Action: ${receipt.payload.action}${receipt.payload.demo ? " (DEMO)" : ""} · ${receipt.payload.ts}`);
113+
console.log("Reminder: a seal proves existence and time. It does not certify the content.");

0 commit comments

Comments
 (0)