// CAIRN/1 — a store-and-forward courier for agents with no network and no cellular radio. // // A cairn is a stack of stones travellers add to as they pass, so the next traveller finds // the trail. That is the design: no internet, no DNS, no certificate authority, no GSM, no // clock synchronisation, no shared secret arranged in advance, no registry of who exists. // Bundles are carried by whatever link two agents happen to share — radio, a cable, a memory // card, a speaker — and every node they rest on forwards them onward. // // Everything a caller needs is here so the running implementation can be SERVED VERBATIM and // checked against the published spec, rather than trusted. No dependencies. import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; export const VERSION = 1; const FLAG_BROADCAST = 0x01; const ID_BYTES = 16; export const MAX_TOLL_BITS = 20; // 2^20 hashes ≈ 2.5s; the ceiling keeps one request bounded export const MAX_PAYLOAD = 4096; /** * The bundle layout, little-endian. The Ed25519 signature covers every byte before it, so a * relay can never rewrite a bundle: hop and copy budgets are LOCAL per-node counters, not * fields on the wire. The toll (proof of work) is hashed over the same prefix. */ export const LAYOUT = [ { name: "version", size: 1, note: "protocol version, currently 1" }, { name: "flags", size: 1, note: "bit0 = broadcast" }, { name: "created", size: 4, note: "unix seconds, from the ORIGIN's clock only" }, { name: "expires", size: 4, note: "advisory: clocks are not synchronised between nodes" }, { name: "hopsLeft", size: 1, note: "advisory on the wire; the enforcing counter is local" }, { name: "copiesLeft", size: 1, note: "spray budget, not a hop count" }, { name: "nonce", size: 4, note: "searched until sha256(prefix) meets the recipient's toll" }, { name: "originId", size: -1, note: "1 length byte then sha256(pubkey)[0..8] as hex — self-certifying" }, { name: "destId", size: -2, note: "1 length byte then a destination id, or the byte '*' for broadcast" }, { name: "payloadLength", size: 4, note: "byte length of the opaque payload" }, { name: "payload", size: -3, note: "opaque; forwarding is NOT confidentiality, wrap it if it matters" }, { name: "publicKey", size: 32, note: "the origin's raw Ed25519 key; the address is derived from it" }, { name: "signature", size: 64, note: "Ed25519 over every preceding byte" }, ]; // ---------------------------------------------------------------- identity export function newIdentity() { const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); const raw = publicKey.export({ type: "spki", format: "der" }).subarray(-32); const id = crypto.createHash("sha256").update(raw).digest("hex").slice(0, ID_BYTES * 2); return { id, publicKey, privateKey, raw }; } const rawPub = (pub) => pub.export({ type: "spki", format: "der" }).subarray(-32); export const addressOf = (raw) => crypto.createHash("sha256").update(raw).digest("hex").slice(0, ID_BYTES * 2); // ---------------------------------------------------------------- codec export function encodeBundle(b) { const parts = []; const head = Buffer.alloc(16); head[0] = VERSION; head[1] = b.broadcast ? FLAG_BROADCAST : 0; head.writeUInt32LE(b.created | 0, 2); head.writeUInt32LE(b.expires | 0, 6); head[10] = b.hopsLeft & 0xff; head[11] = b.copiesLeft & 0xff; head.writeUInt32LE(b.nonce >>> 0, 12); parts.push(head); const origin = Buffer.from(b.origin, "utf8"); parts.push(Buffer.from([origin.length]), origin); const dest = Buffer.from(b.broadcast ? "*" : b.dest, "utf8"); parts.push(Buffer.from([dest.length]), dest); const pl = Buffer.from(b.payload); const plHead = Buffer.alloc(4); plHead.writeUInt32LE(pl.length, 0); parts.push(plHead, pl, Buffer.from(b.pubRaw)); return Buffer.concat(parts); } export function decodeBundle(buf) { if (buf.length < 16 + 1 + ID_BYTES + 1 + 1 + 4 + 32 + 64) throw new Error("cairn: too short"); let o = 0; const u8 = () => buf[o++]; const u32 = () => { const v = buf.readUInt32LE(o); o += 4; return v; }; const version = u8(); if (version !== VERSION) throw new Error("cairn: unknown version " + version); const flags = u8(); const created = u32(), expires = u32(); const hopsLeft = u8(), copiesLeft = u8(), nonce = u32(); const olen = u8(); const origin = buf.subarray(o, o + olen).toString("utf8"); o += olen; const dlen = u8(); const destRaw = buf.subarray(o, o + dlen).toString("utf8"); o += dlen; const plen = u32(); const payload = buf.subarray(o, o + plen); o += plen; const pubRaw = buf.subarray(o, o + 32); o += 32; const sig = buf.subarray(o, o + 64); o += 64; if (o !== buf.length) throw new Error("cairn: trailing bytes"); const unsigned = buf.subarray(0, buf.length - 64); let pub = null, keyReadable = true; try { pub = crypto.createPublicKey({ key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), pubRaw]), format: "der", type: "spki" }); } catch { keyReadable = false; } return { version, flags, created, expires, hopsLeft, copiesLeft, nonce, origin, dest: destRaw === "*" ? null : destRaw, broadcast: !!(flags & FLAG_BROADCAST), destRaw, payload, pub, pubRaw, sig, unsigned, keyReadable, claimedIdMatchesKey: addressOf(pubRaw) === origin, payloadUtf8: (() => { try { return new TextDecoder("utf-8", { fatal: true }).decode(payload); } catch { return null; } // not valid UTF-8, so it is bytes rather than text })(), }; } export function verifyBundle(b) { if (!b.keyReadable) return { ok: false, why: "public key is not a valid Ed25519 key" }; if (!b.claimedIdMatchesKey) return { ok: false, why: "address does not match the public key" }; try { if (!crypto.verify(null, b.unsigned, b.pub, b.sig)) return { ok: false, why: "signature invalid" }; } catch { return { ok: false, why: "signature could not be checked" }; } return { ok: true, why: "signature valid and the address is derived from the key" }; } /** The toll, in leading zero bits of sha256 over the unsigned bundle. */ export function powBits(buf) { const h = crypto.createHash("sha256").update(buf).digest(); let z = 0; for (const byte of h) { if (byte === 0) { z += 8; continue; } z += Math.clz32(byte) - 24; break; } return z; } export const checkPow = (b, bits) => powBits(b.unsigned) >= bits; export function mintPow(b, bits, budget = 1 << 26) { const t0 = process.hrtime.bigint(); for (let n = 0; n < budget; n++) { b.nonce = n >>> 0; if (powBits(encodeBundle(b)) >= bits) { return { nonce: b.nonce, tries: n + 1, ms: Number(Number(process.hrtime.bigint() - t0) / 1e6) }; } } throw new Error("cairn: proof-of-work budget exhausted at " + bits + " bits"); } export function mint(payload, tollBits, opts = {}) { if (!Number.isInteger(tollBits) || tollBits < 1 || tollBits > MAX_TOLL_BITS) { throw new Error(`tollBits must be an integer 1..${MAX_TOLL_BITS}`); } const id = opts.identity || newIdentity(); const now = Math.floor(Date.now() / 1000); const b = { created: now, expires: now + (opts.ttlSeconds ?? 86400 * 7), hopsLeft: opts.hops ?? 8, copiesLeft: opts.copies ?? 4, nonce: 0, origin: id.id, dest: opts.dest || id.id, broadcast: !!opts.broadcast, payload: Buffer.from(payload), pubRaw: id.raw, }; const proof = mintPow(b, tollBits); const sealed = Buffer.concat([encodeBundle(b), crypto.sign(null, encodeBundle(b), id.privateKey)]); return { sealed, identity: id, proof, tollBits }; } /** Byte map for a real bundle, so a reader can see which region is which. */ export function layoutOf(buf) { const map = []; let o = 0; const push = (name, size, note) => { map.push({ name, start: o, end: o + size, size, note }); o += size; }; push("version", 1, LAYOUT[0].note); push("flags", 1, LAYOUT[1].note); push("created", 4, LAYOUT[2].note); push("expires", 4, LAYOUT[3].note); push("hopsLeft", 1, LAYOUT[4].note); push("copiesLeft", 1, LAYOUT[5].note); push("nonce", 4, LAYOUT[6].note); const olen = buf[16]; push("originId", 1 + olen, LAYOUT[7].note); const dlen = buf[o]; // the length byte sits immediately after the origin id push("destId", 1 + dlen, LAYOUT[8].note); const plen = buf.readUInt32LE(o); push("payloadLength", 4, LAYOUT[9].note); push("payload", plen, LAYOUT[10].note); push("publicKey", 32, LAYOUT[11].note); push("signature", 64, LAYOUT[12].note); return map; } // ---------------------------------------------------------------- sneakernet link // A physical medium with no network: removable storage. A directory IS the messenger bag. export function sneakernetDemo() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cairn-usb-")); try { const alice = newIdentity(), bob = newIdentity(), carol = newIdentity(); const transcript = []; const bagDrop = (buf) => fs.writeFileSync(path.join(dir, "bundle.cairn"), buf); const bagCollect = () => { const f = path.join(dir, "bundle.cairn"); if (!fs.existsSync(f)) return []; const b = fs.readFileSync(f); fs.unlinkSync(f); return [b]; }; const { sealed, proof } = mint(Buffer.from(JSON.stringify({ task: "verify relay logs", reward: "12 units" })), 12, { identity: alice, dest: bob.id, copies: 2 }); transcript.push({ step: "seal", detail: `${sealed.length} bytes, toll 12 bits paid in ${proof.tries} hashes (${proof.ms.toFixed(1)} ms)`, ok: true }); bagDrop(sealed); transcript.push({ step: "drop on removable media", detail: `1 bundle on the medium (${dir.replace(os.tmpdir(), "$TMP")})`, ok: true }); const carried = bagCollect(); const env = decodeBundle(carried[0]); transcript.push({ step: "courier carries it", detail: `courier lifted ${carried.length}; envelope reads ${env.origin.slice(0, 12)}… -> ${env.dest.slice(0, 12)}…, ${env.payload.length} payload bytes. The courier can see the envelope, not the contents.`, ok: true }); const v = verifyBundle(env); const toll = checkPow(env, 12); const deliveredToBob = env.dest === bob.id; transcript.push({ step: "destination ingests", detail: `signature ${v.ok ? "valid" : "INVALID"}; address matches key: ${env.claimedIdMatchesKey}; toll paid: ${toll}; delivered to bob: ${deliveredToBob}`, ok: v.ok && toll && deliveredToBob }); // impersonation: keep the victim's public key, sign with your own const impostor = { created: env.created, expires: env.expires, hopsLeft: 8, copiesLeft: 1, nonce: 0, origin: alice.id, dest: bob.id, broadcast: false, payload: Buffer.from("i am alice, honest"), pubRaw: alice.raw }; mintPow(impostor, 12); const impostorSealed = Buffer.concat([encodeBundle(impostor), crypto.sign(null, encodeBundle(impostor), carol.privateKey)]); let r1; try { r1 = verifyBundle(decodeBundle(impostorSealed)); } catch (e) { r1 = { ok: false, why: e.message }; } transcript.push({ step: "forgery: sign with your own key, claim the victim's", detail: `rejected — ${r1.why}`, ok: !r1.ok }); // substitution: carry your own key under the victim's address const swap = { ...impostor, nonce: 0, pubRaw: carol.raw }; mintPow(swap, 12); const swapSealed = Buffer.concat([encodeBundle(swap), crypto.sign(null, encodeBundle(swap), carol.privateKey)]); let r2; try { r2 = verifyBundle(decodeBundle(swapSealed)); } catch (e) { r2 = { ok: false, why: e.message }; } transcript.push({ step: "forgery: carry your own key under the victim's address", detail: `rejected — ${r2.why}`, ok: !r2.ok }); // unpaid spam const cheap = { ...impostor, nonce: 0, pubRaw: alice.raw }; const cheapSealed = Buffer.concat([encodeBundle(cheap), crypto.sign(null, encodeBundle(cheap), alice.privateKey)]); const cheapDecoded = decodeBundle(cheapSealed); const cheapToll = checkPow(cheapDecoded, 12); transcript.push({ step: "spam with no toll paid", detail: `rejected — toll unpaid (12 bits required, got ${powBits(cheapDecoded.unsigned)})`, ok: !cheapToll }); // one byte flipped anywhere in the signature const flipped = Buffer.from(sealed); flipped[flipped.length - 1] ^= 0x01; let r3; try { r3 = verifyBundle(decodeBundle(flipped)); } catch (e) { r3 = { ok: false, why: e.message }; } transcript.push({ step: "tamper: flip one bit of the signature", detail: `rejected — ${r3.why}`, ok: !r3.ok }); return { dir: dir.replace(os.tmpdir(), "$TMP"), transcript, allRejected: transcript.every((t) => t.ok) }; } finally { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* scratch dir, best effort */ } } } // ---------------------------------------------------------------- mesh simulation // A reduced but real discrete-event simulation: agents wander a field, meet when close, and // carry bundles for each other. Reported numbers state their own parameters so they cannot be // read without knowing what produced them. export function meshSim({ nodes = 30, grid = 80, radius = 6, rounds = 300, origins = 5, perOrigin = 6, toll = 8, seed = 12345 } = {}) { const rngOf = (s) => { let x = s >>> 0; return () => { x = (1664525 * x + 1013904223) >>> 0; return x / 4294967296; }; }; const oneRun = (copies) => { const rnd = rngOf(seed); const ids = Array.from({ length: nodes }, () => newIdentity()); const pos = ids.map(() => ({ x: rnd() * grid, y: rnd() * grid })); const vel = ids.map(() => ({ dx: (rnd() - 0.5) * 3, dy: (rnd() - 0.5) * 3 })); const spools = ids.map(() => new Map()); const delivered = new Set(); const born = new Map(); let transmissions = 0, sno = 0; for (let o = 0; o < origins; o++) for (let k = 0; k < perOrigin; k++) { const dest = Math.floor(rnd() * nodes); if (dest === o) continue; const { sealed } = mint(Buffer.from("p" + sno), toll, { identity: ids[o], dest: ids[dest].id, copies }); const id = crypto.createHash("sha256").update(sealed).digest("hex").slice(0, 24); spools[o].set(id, { buf: sealed, dest: ids[dest].id, copies, hops: 0 }); born.set(id, 0); sno++; } const meets = (a, b) => { const dx = pos[a].x - pos[b].x, dy = pos[a].y - pos[b].y; return dx * dx + dy * dy <= radius * radius; }; const transfer = (from, to, round) => { for (const [id, e] of [...spools[from]]) { if (e.dest === ids[to].id) { spools[to].set(id, { ...e, hops: e.hops + 1 }); spools[from].delete(id); transmissions++; if (!delivered.has(id)) delivered.add(id); continue; } if (e.hops + 1 > 8) { spools[from].delete(id); continue; } if (e.copies > 1) { const give = Math.floor(e.copies / 2); e.copies -= give; spools[to].set(id, { ...e, copies: give, hops: e.hops + 1 }); transmissions++; } } }; for (let round = 1; round <= rounds; round++) { for (let i = 0; i < nodes; i++) { pos[i].x += vel[i].dx; pos[i].y += vel[i].dy; if (pos[i].x < 1 || pos[i].x > grid - 1) { vel[i].dx *= -1; pos[i].x = Math.max(1, Math.min(grid - 1, pos[i].x)); } if (pos[i].y < 1 || pos[i].y > grid - 1) { vel[i].dy *= -1; pos[i].y = Math.max(1, Math.min(grid - 1, pos[i].y)); } if (round % 17 === 0) { vel[i].dx = (rnd() - 0.5) * 3; vel[i].dy = (rnd() - 0.5) * 3; } } for (let i = 0; i < nodes; i++) for (let j = i + 1; j < nodes; j++) if (meets(i, j)) { transfer(i, j, round); transfer(j, i, round); } } const total = born.size; return { copies, originated: total, delivered: delivered.size, ratio: Number((delivered.size / total).toFixed(3)), transmissions }; }; const runs = [1, 2, 4, 8, 32].map(oneRun); return { protocol: "cairn/1", measured: "live on this relay, on request, and cached for an hour", parameters: { nodes, grid, contactRadius: radius, rounds, origins, perOrigin, tollBits: toll, bundles: origins * perOrigin, seed }, caveat: "a random-walk mobility model, not real traces — real agents on real vehicles have different contact statistics and these numbers will move", runs: runs.map((r) => ({ ...r, transmissionsPerDelivery: r.delivered ? Number((r.transmissions / r.delivered).toFixed(1)) : null })), }; } // ---------------------------------------------------------------- selftest export function selftest() { const t0 = Date.now(); const alice = newIdentity(), bob = newIdentity(); const out = { protocol: "cairn/1", version: VERSION, requirements: ["no internet", "no DNS", "no certificate authority", "no GSM or any cellular radio", "no clock synchronisation", "no shared secret arranged in advance", "no registry of who exists"] }; out.identity = { selfCertifying: true, idExample: alice.id, bytesInAddress: ID_BYTES, note: "An identity IS its public key: the address is the first 16 hex of sha256(pubkey), so any node can confirm that a bundle's claimed origin really owns that address with a single signature check. Nothing to register, nothing to look up, no trust-on-first-use.", }; const payload = crypto.randomBytes(256); const { sealed, proof } = mint(payload, 8, { identity: alice, dest: bob.id, copies: 4 }); const opened = decodeBundle(sealed); out.roundTrip = { totalBytes: sealed.length, payloadBytes: payload.length, overheadBytes: sealed.length - payload.length, payloadExact: Buffer.compare(opened.payload, payload) === 0, signatureValid: verifyBundle(opened).ok, addressSelfConsistent: opened.claimedIdMatchesKey, tollPaid: checkPow(opened, 8), hashesToPay: proof.tries, }; out.overheadForShortMessage = (() => { const r = mint(Buffer.from("x".repeat(48)), 8, { identity: alice, dest: bob.id }); return { payloadBytes: 48, totalBytes: r.sealed.length, overheadBytes: r.sealed.length - 48 }; })(); const t = Buffer.from(sealed); t[20] ^= 0x01; let caught = false; try { const o = decodeBundle(t); caught = !verifyBundle(o).ok || !o.claimedIdMatchesKey; } catch { caught = true; } out.tamperRejected = caught; // The sender's cost is a random variable (~2^bits trials), so report a mean over samples // rather than one draw: a single sample can be off by an order of magnitude and would look // like a measurement when it is noise. The verifier's cost is one hash, always. out.toll = []; for (const bits of [8, 12, 16]) { const samples = []; for (let s = 0; s < 5; s++) { const bb = { created: s, expires: 9e9, hopsLeft: 8, copiesLeft: 1, nonce: 0, origin: alice.id, dest: bob.id, broadcast: false, payload: Buffer.from("x".repeat(64)), pubRaw: alice.raw }; samples.push(mintPow(bb, bits)); } const tries = samples.map((r) => r.tries); const meanTries = Math.round(tries.reduce((a, b) => a + b, 0) / tries.length); out.toll.push({ bits, samples: samples.length, meanHashesForTheSender: meanTries, minHashes: Math.min(...tries), maxHashes: Math.max(...tries), meanSenderMs: Number((samples.reduce((a, r) => a + r.ms, 0) / samples.length).toFixed(1)), expectedHashes: Math.pow(2, bits), verifierWork: "one hash", }); } const N = 1000, bufs = []; for (let i = 0; i < 50; i++) bufs.push(mint(Buffer.from("y".repeat(128)), 8, { identity: alice, dest: bob.id }).sealed); const v0 = process.hrtime.bigint(); let ok = 0; for (let i = 0; i < N; i++) { const d = decodeBundle(bufs[i % bufs.length]); if (verifyBundle(d).ok && checkPow(d, 8)) ok++; } const v1 = process.hrtime.bigint(); out.verification = { bundles: N, verified: ok, ms: Number((Number(v1 - v0) / 1e6).toFixed(1)), perBundleUs: Number((Number(v1 - v0) / 1000 / N).toFixed(1)), note: "decode + Ed25519 verify + toll check, i.e. what a forwarding node does per bundle" }; out.sneakernet = sneakernetDemo(); out.ms = Date.now() - t0; out.honest = [ "Delivery is probabilistic and depends on two agents physically meeting. This is not a socket and never will be.", "Transferable offline credit is double-spendable when nodes cannot see each other. The toll is WORK for exactly that reason: work cannot be faked or double-spent.", "A forwarding node can read the payload. Forwarding is not confidentiality; wrap the payload if it matters.", "No clocks are synchronised, so expiry is advisory and hops and copies are the real controls.", "The transport capacities people quote for LoRa, BLE, ultrasonic and the rest are published datasheets, not measurements taken here. Only the protocol layer above is measured on this box.", ]; return out; } export function sourceOf() { return fs.readFileSync(new URL(import.meta.url), "utf8"); } // ---------------------------------------------------------------- routes const HONEST = [ "Delivery is probabilistic and depends on two agents physically meeting. This is not a socket and never will be.", "Transferable offline credit is double-spendable when nodes cannot see each other. The toll is WORK for exactly that reason: work cannot be faked and cannot be double-spent.", "A forwarding node can read the payload. Forwarding is not confidentiality; encrypt the payload separately if it matters.", "No clocks are synchronised between nodes, so expiry is advisory. Hops and copies are the real controls.", "Transport rates people quote for LoRa, BLE, ultrasonic and the rest are published datasheets. Only the protocol layer above them is measured on this box.", "The mesh numbers come from a random-walk mobility model, not real traces. Real agents on real vehicles have different contact statistics and the numbers will move.", ]; export function register(store) { let selfCache = null, selfAt = 0, meshCache = null, meshAt = 0; const SELF_TTL = 10 * 60 * 1000, MESH_TTL = 60 * 60 * 1000; return (app, ctx) => { app.get("/v1/cairn", (_req, res) => { res.json({ protocol: "cairn/1", summary: "A store-and-forward courier for agents with no network and no cellular radio. Bundles are carried by whatever link two nodes happen to share — radio, a cable, a memory card, a speaker — and every node they rest on forwards them onward.", requires: [], doesNotRequire: ["internet", "DNS", "a certificate authority", "GSM or any cellular radio", "clock synchronisation between nodes", "any shared secret arranged in advance", "a registry of who exists"], naming: "An identity IS its public key: the address is the first 16 hex of sha256(pubkey), so any node can confirm a bundle's claimed origin really owns that address with one signature check. Nothing to look up.", paying: "You cannot settle on-chain with no network, and transferable offline credit is double-spendable by construction. So the toll is WORK: the recipient advertises a difficulty and a bundle must hash below it. The sender pays 2^D hashes; the verifier pays one.", endpoints: { spec: "/v1/cairn/spec", source: "/v1/cairn/source", selftest: "/v1/cairn/selftest", mesh: "/v1/cairn/mesh", mint: "POST /v1/cairn/mint", verify: "POST /v1/cairn/verify", page: "/cairn.html", }, limits: { maxTollBits: MAX_TOLL_BITS, maxPayloadBytes: MAX_PAYLOAD }, honest: HONEST, }); }); app.get("/v1/cairn/spec", (_req, res) => { res.json({ protocol: "cairn/1", version: VERSION, endianness: "little", signedPrefix: "The Ed25519 signature covers every byte before it. The proof of work is hashed over the same prefix, nonce included.", immutability: "A relay must NEVER rewrite a signed bundle. Hop and copy budgets are therefore LOCAL per-node counters, not fields a forwarder edits — copies halve on each handoff so the budget is conserved rather than multiplied.", layout: LAYOUT, routing: "Spray-and-wait. A bundle starts with N copies; each handoff gives the peer half and keeps half. The final copy is only handed to a peer that has recently met the destination, learned from an encounter summary exchanged on contact.", toll: { formula: "sha256(unsigned bundle) must have >= D leading zero bits", sender: "~2^D hashes", verifier: "1 hash", ceiling: MAX_TOLL_BITS }, honest: HONEST, }); }); app.get("/v1/cairn/source", (_req, res) => { res.type("text/plain; charset=utf-8").send(sourceOf()); }); app.get("/v1/cairn/selftest", (_req, res) => { const now = Date.now(); if (!selfCache || now - selfAt > SELF_TTL) { selfCache = selftest(); selfAt = now; } res.json({ ...selfCache, cachedAt: new Date(selfAt).toISOString(), cacheSeconds: SELF_TTL / 1000 }); }); app.get("/v1/cairn/mesh", (_req, res) => { const now = Date.now(); if (!meshCache || now - meshAt > MESH_TTL) { meshCache = meshSim(); meshAt = now; } res.json({ ...meshCache, cachedAt: new Date(meshAt).toISOString(), cacheSeconds: MESH_TTL / 1000 }); }); // Mint a bundle. A fresh identity per call: the address is derived from the key, so there // is nothing to register and nothing to leak. app.post("/v1/cairn/mint", (req, res) => { const raw = req.body?.payload; const payload = typeof raw === "string" && raw.length ? Buffer.from(raw, "utf8") : crypto.randomBytes(128); if (payload.length > MAX_PAYLOAD) return res.status(400).json({ error: `payload must be <= ${MAX_PAYLOAD} bytes` }); const tollBits = req.body?.tollBits === undefined ? 12 : Number(req.body.tollBits); if (!Number.isInteger(tollBits) || tollBits < 1 || tollBits > MAX_TOLL_BITS) { return res.status(400).json({ error: `tollBits must be an integer 1..${MAX_TOLL_BITS}` }); } const copies = Math.min(64, Math.max(1, Number(req.body?.copies) || 4)); const hops = Math.min(32, Math.max(1, Number(req.body?.hops) || 8)); const broadcast = !!req.body?.broadcast; try { const { sealed, identity, proof } = mint(payload, tollBits, { copies, hops, broadcast, dest: req.body?.dest }); res.json({ bundle: sealed.toString("base64"), bytes: sealed.length, payloadBytes: payload.length, overheadBytes: sealed.length - payload.length, tollBits, proof: { hashes: proof.tries, ms: Number(proof.ms.toFixed(1)) }, origin: identity.id, originPublicKey: identity.raw.toString("hex"), broadcast, copies, hops, layout: layoutOf(sealed), verifyIt: "POST /v1/cairn/verify with {\"bundle\": \"\"}", tamperIt: "flip any byte of the base64-decoded bundle and verify again — it will be rejected", }); } catch (e) { res.status(400).json({ error: e.message }); } }); app.post("/v1/cairn/verify", (req, res) => { const b64 = req.body?.bundle; if (typeof b64 !== "string" || !b64.length) return res.status(400).json({ error: "bundle (base64) is required" }); if (b64.length > 65536) return res.status(400).json({ error: "bundle too large" }); let buf; try { buf = Buffer.from(b64, "base64"); } catch { return res.status(400).json({ error: "bundle is not valid base64" }); } if (!buf.length) return res.status(400).json({ error: "bundle decoded to zero bytes" }); const requiredBits = Math.min(MAX_TOLL_BITS, Math.max(0, Number(req.body?.tollBits) || 8)); let d; try { d = decodeBundle(buf); } catch (e) { return res.json({ ok: false, why: e.message, bytes: buf.length }); } const v = verifyBundle(d); const found = powBits(d.unsigned); const tollPaid = found >= requiredBits; res.json({ ok: v.ok && tollPaid, why: !v.ok ? v.why : (!tollPaid ? `toll unpaid: ${found} leading zero bits found, ${requiredBits} required` : v.why), checks: { addressMatchesPublicKey: d.claimedIdMatchesKey, signatureValid: v.ok, tollBitsFound: found, tollBitsRequired: requiredBits, tollPaid, }, decoded: { version: d.version, origin: d.origin, dest: d.dest, broadcast: d.broadcast, created: d.created, expires: d.expires, hopsLeft: d.hopsLeft, copiesLeft: d.copiesLeft, nonce: d.nonce, payloadBytes: d.payload.length, payloadText: d.payloadUtf8, // null when the payload is not valid UTF-8, i.e. bytes not text publicKey: d.pubRaw.toString("hex"), }, bytes: buf.length, layout: (() => { try { return layoutOf(buf); } catch { return null; } })(), }); }); }; }