Skip to content

Commit 07242ea

Browse files
committed
j
1 parent d46bfaf commit 07242ea

3 files changed

Lines changed: 67 additions & 4 deletions

File tree

worker/src/catalog.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,18 @@ function fullRows(state) {
248248
}
249249

250250
export async function observatoryJson(env) {
251-
let state = null;
251+
let state = null, list = null;
252252
try { state = JSON.parse(await env.COHERENCE.get(STATE_KEY)); } catch { /* vuoto */ }
253-
const rows = fullRows(state);
253+
try { list = JSON.parse(await env.COHERENCE.get(LIST_KEY)); } catch { /* vuoto */ }
254+
const inRotation = list?.urls?.length || Object.keys(state?.entries || {}).length;
255+
const rows = fullRows(state).filter((r) => !list?.urls || list.urls.includes(r.url));
254256
const alive = rows.filter((r) => r.alive).length;
255257
return {
256258
name: "GBLIN x402 Uptime Observatory",
257259
generated_at: new Date(state?.updatedAt || Date.now()).toISOString(),
258260
stable_url: "https://gblin-mcp.gblin-mcp-worker.workers.dev/observatory.json",
259261
methodology: METHODOLOGY,
260-
summary: { tracked: rows.length, alive_now: alive, alive_pct: rows.length ? Math.round((1000 * alive) / rows.length) / 10 : 0, in_rotation: Object.keys(state?.entries || {}).length, note: rows.length < Object.keys(state?.entries || {}).length ? "rule change in progress: only endpoints already re-probed under the current rule are counted; the rest re-enter within ~3h" : undefined },
262+
summary: { tracked: rows.length, alive_now: alive, alive_pct: rows.length ? Math.round((1000 * alive) / rows.length) / 10 : 0, in_rotation: inRotation, note: rows.length < inRotation ? "rule change in progress: only endpoints already re-probed under the current rule are counted; the rest re-enter within ~3h" : undefined },
261263
endpoints: rows,
262264
free_market_risk_regime: "https://gblin-mcp.gblin-mcp-worker.workers.dev/regime",
263265
operator: "gblin.digital (ERC-8004 agent #59286 on Base) — our own endpoints appear in the table under the same rules",

worker/src/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { catalogTick, catalogReport, catalogFull, observatoryPage, observatoryJs
2929
// trasparenza terzi (C2SP tlog-cosignature v1). Primo log: markovianprotocol.com,
3030
// su loro invito. Zero costo: 1 lettura + 1 firma per tick; niente chain.
3131
// Secret WITNESS_KEY assente → disattivato in silenzio (fail-safe).
32-
import { witnessTick, witnessIndex, witnessLatestNote, WITNESSED_LOGS } from "./witness.mjs";
32+
import { witnessTick, witnessIndex, witnessLatestNote, witnessAddCheckpoint, WITNESSED_LOGS } from "./witness.mjs";
3333

3434
const GBLIN = "0x36C81d7E1966310F305eA637e761Cf77F90852f0";
3535
const BASKET_SELECTOR = "0x8c7e0875"; // basket(uint256)
@@ -904,6 +904,12 @@ export default {
904904

905905
// WITNESS — indice pubblico (chiave di verifica, ultimo checkpoint cofirmato per log)
906906
// e la nota cofirmata in chiaro, nel formato che qualsiasi verificatore C2SP legge.
907+
if (url.pathname === "/witness/add-checkpoint" && request.method === "POST") {
908+
const bodyText = await request.text();
909+
if (bodyText.length > 65536) return new Response("too large\n", { status: 413 });
910+
const r = await witnessAddCheckpoint(env, bodyText);
911+
return new Response(r.body, { status: r.status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
912+
}
907913
if (url.pathname === "/witness" && request.method === "GET") {
908914
return json(await witnessIndex(env), 200, { "cache-control": "public, max-age=60" });
909915
}

worker/src/witness.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,60 @@ export async function witnessTick(env, fetchImpl = fetch) {
219219
return out;
220220
}
221221

222+
223+
// ---------- push side: c2sp.org/tlog-witness (the LOG calls us) ----------
224+
// POST /witness/add-checkpoint body = "old <size>\n" + proof lines (b64, one per line) + "\n" + <signed checkpoint note>
225+
// 200 → our cosignature line(s); 400 malformed; 403 unknown log / not signed by the pinned key;
226+
// 409 `old` ≠ the size we hold (body = our size, decimal + "\n"); 422 consistency proof invalid / tree shrank / fork.
227+
// Same KV state as the passive tick, so pushed and fetched checkpoints can never disagree.
228+
export async function witnessAddCheckpoint(env, bodyText) {
229+
if (!env.COHERENCE || !env.WITNESS_KEY) return { status: 503, body: "witness not armed\n" };
230+
let keyPair;
231+
try { keyPair = parseWitnessSecret(env.WITNESS_KEY); } catch { return { status: 503, body: "witness not armed\n" }; }
232+
const sep = bodyText.indexOf("\n\n");
233+
if (sep < 0) return { status: 400, body: "malformed: no blank line between proof and checkpoint\n" };
234+
const head = bodyText.slice(0, sep).split("\n");
235+
const m = /^old (\d+)$/.exec(head[0] || "");
236+
if (!m) return { status: 400, body: "malformed: first line must be 'old <size>'\n" };
237+
const old = Number(m[1]);
238+
let proof;
239+
try { proof = head.slice(1).filter((l) => l.length > 0).map(unb64); } catch { return { status: 400, body: "malformed: proof lines must be base64\n" }; }
240+
let note;
241+
try { note = parseNote(bodyText.slice(sep + 2)); } catch (e) { return { status: 400, body: `malformed checkpoint: ${e.message}\n` }; }
242+
const log = WITNESSED_LOGS.find((l) => l.origin === note.origin);
243+
if (!log) return { status: 403, body: "unknown log\n" };
244+
let sigOk = false;
245+
try { sigOk = await verifyLogSignature(note, log.vkey); } catch { sigOk = false; }
246+
if (!sigOk) return { status: 403, body: "checkpoint not signed by the pinned log key\n" };
247+
248+
const kLast = `witness:${log.id}:last`, kCount = `witness:${log.id}:count`, kErr = `witness:${log.id}:err`;
249+
let prev = null;
250+
try { prev = JSON.parse((await env.COHERENCE.get(kLast)) || "null"); } catch { prev = null; }
251+
const held = prev ? prev.size : 0;
252+
if (old !== held) return { status: 409, body: `${held}\n` };
253+
if (prev) {
254+
if (note.size < prev.size) return { status: 422, body: "tree shrank\n" };
255+
if (note.size === prev.size) {
256+
if (b64(note.root) !== prev.root) return { status: 422, body: "same size, different root\n" };
257+
// nothing new: re-cosign the head we already hold (fresh timestamp)
258+
} else if (!(await verifyConsistency(prev.size, note.size, unb64(prev.root), note.root, proof))) {
259+
return { status: 422, body: "consistency proof invalid\n" };
260+
}
261+
} else if (proof.length !== 0) {
262+
return { status: 400, body: "no proof expected for old 0\n" };
263+
}
264+
const { line, ts } = await cosign(note, keyPair);
265+
if (!prev || note.size > prev.size) {
266+
const text = bodyText.slice(sep + 2);
267+
const cosignedNote = text.endsWith("\n") ? text + line + "\n" : text + "\n" + line + "\n";
268+
await env.COHERENCE.put(kLast, JSON.stringify({ size: note.size, root: b64(note.root), ts, cosignedNote, firstSeen: prev?.firstSeen || ts, via: "push" }));
269+
const count = Number((await env.COHERENCE.get(kCount)) || 0) + 1;
270+
await env.COHERENCE.put(kCount, String(count));
271+
await env.COHERENCE.delete(kErr);
272+
}
273+
return { status: 200, body: line + "\n" };
274+
}
275+
222276
// ---------- public read side ----------
223277
export async function witnessIndex(env) {
224278
let verifierKey = null;
@@ -246,6 +300,7 @@ export async function witnessIndex(env) {
246300
cadence: "every 10 minutes (same heartbeat as the coherence automaton); unchanged tree size → no new signature",
247301
armed: !!verifierKey,
248302
logs,
303+
push_endpoint: "POST /witness/add-checkpoint — c2sp.org/tlog-witness (body: 'old <size>', consistency proof lines, blank line, signed checkpoint; 200 = cosignature line, 409 = size we hold, 422 = proof invalid)",
249304
honest_note: "A cosignature says only: 'at this time we saw this tree head and it was consistent with the previous one we saw'. It is not an endorsement of the log's contents.",
250305
};
251306
}

0 commit comments

Comments
 (0)