From 09a93e75b99ed8c18ad585ed6065170d57da5ccf Mon Sep 17 00:00:00 2001 From: Micka Delcato Date: Thu, 7 May 2026 10:35:45 +0200 Subject: [PATCH 1/2] =?UTF-8?q?rfc:=20compounding=20intelligence=20roadmap?= =?UTF-8?q?=20=E2=80=94=20VISION=20+=20RFC-001/002/003=20+=20receipt=20pro?= =?UTF-8?q?totype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the long-term technical direction for moving Hyperspace from a parallel intelligence network (today) to a compounding intelligence network through five structural transformations. VISION.md frames the five transformations: 1. Verifiable Research (RFC-001 — written here) 2. Curriculum DAG (RFC-002 — written here) 3. Proof-of-Useful-Work (RFC-003 — written here) 4. Modular Agents (stub for future RFC-004) 5. Reality Anchoring (stub for future RFC-005) RFC-001 (Verifiable Research) is the foundation: every experiment carries a signed, schema-conformant receipt with a graduated proof ladder (signed → replay → zk-jolt → zk-groth16 → tee-attestation). Reference implementation included and verified end-to-end: - schemas/research-receipt-v1.schema.json — JSON Schema definition - scripts/sign-receipt.js — Ed25519 signer - scripts/validate-receipt.js — reference validator - projects/_template/baseline/example-receipt.json — worked example RFC-002 (Curriculum DAG) builds on receipts: projects declare prerequisites, agents are scheduled along the DAG, weights transfer along edges. Compounding learning instead of parallel learning. RFC-003 (PoUW Consensus) replaces the unstable Narwhal/Bullshark mining with research-as-mining: every block carries a verifiable research receipt above chain difficulty. Solves chain instability (#15, #18) and economic misalignment with the same primitive. Also adds: - docs/rfcs/README.md — RFC process documentation - docs/rfcs/0000-template.md — template for future RFCs The reference validator is functional today: signing produces a valid receipt, validate-receipt.js verifies signature + receiptId + schema in sub-second time. End-to-end demo in the RFC-001 §Reference implementation. --- docs/VISION.md | 172 +++++++++ docs/rfcs/0000-template.md | 65 ++++ docs/rfcs/README.md | 65 ++++ docs/rfcs/RFC-001-verifiable-research.md | 365 ++++++++++++++++++ docs/rfcs/RFC-002-curriculum-dag.md | 274 +++++++++++++ docs/rfcs/RFC-003-pouw-consensus.md | 295 ++++++++++++++ .../baseline/example-receipt-unsigned.json | 38 ++ .../_template/baseline/example-receipt.json | 44 +++ schemas/research-receipt-v1.schema.json | 217 +++++++++++ scripts/sign-receipt.js | 98 +++++ scripts/validate-receipt.js | 239 ++++++++++++ 11 files changed, 1872 insertions(+) create mode 100644 docs/VISION.md create mode 100644 docs/rfcs/0000-template.md create mode 100644 docs/rfcs/README.md create mode 100644 docs/rfcs/RFC-001-verifiable-research.md create mode 100644 docs/rfcs/RFC-002-curriculum-dag.md create mode 100644 docs/rfcs/RFC-003-pouw-consensus.md create mode 100644 projects/_template/baseline/example-receipt-unsigned.json create mode 100644 projects/_template/baseline/example-receipt.json create mode 100644 schemas/research-receipt-v1.schema.json create mode 100644 scripts/sign-receipt.js create mode 100644 scripts/validate-receipt.js diff --git a/docs/VISION.md b/docs/VISION.md new file mode 100644 index 0000000000000..07950eddebbff --- /dev/null +++ b/docs/VISION.md @@ -0,0 +1,172 @@ +# Vision: The Compounding Intelligence Network + +> Hyperspace today is a **parallel** intelligence network — thousands of agents +> working in parallel, sharing experiments via gossip. To become a **compounding** +> intelligence network — where each agent's contribution makes every other agent +> measurably stronger — five structural transformations are required. +> +> This document frames those transformations. Each one removes a different +> ceiling on what the network can become. + +--- + +## Where we are (April 2026) + +What works: +- **27,000+ experiments** completed across 5 research domains +- **695 live agents** with sub-second gossip (libp2p + GossipSub) +- **Distributed training** at 195× compression (5.5 MB → 28 KB per round) +- **CRDT leaderboards** synchronizing peer-best results without coordination +- **Reproducible model** released (qwen2.5-0.5b-hyperspace-v1) with full training metadata + +What blocks us from compounding: + +| Current limit | Symptom | +|---|---| +| **Trust is unverifiable** | Sybil agents can rubber-stamp bad research (RFC #10). Reputation is uptime, not quality. | +| **Research is isolated** | Solving project A doesn't help project B. No curriculum. No transfer learning at the network level. | +| **Consensus is fragile** | The chain forks every 8 hours under steady-state load (#15). The chain layer and research layer are decoupled — neither makes the other stronger. | +| **Agents are monolithic** | An agent is one process, one model, one set of skills. There is no protocol for composing skills across agents. | +| **Evaluation is synthetic** | All scores come from synthetic benchmarks. The network can't tell the difference between "good on the benchmark" and "useful in reality." | + +These are not bugs. They are the structural ceiling. Five transformations break through them. + +--- + +## Transformation 1 — Verifiable Research + +> Every experiment result carries a cryptographic proof that it was actually +> computed. Trust becomes optional; verification is cheap and universal. + +**RFC:** [`docs/rfcs/RFC-001-verifiable-research.md`](rfcs/RFC-001-verifiable-research.md) + +**The change:** A new `receipt.json` schema, signed and zk-attested, accompanies every experiment branch. Validators can replay-verify locally; cheap zk proofs (Groth16 / SP1 / Jolt — already in the project's proof stack) make verification universal. + +**What it unlocks:** +- Eliminates the Sybil-rubber-stamp attack at the root: a Sybil cluster has no advantage if every agent's claim is independently verifiable. +- Turns reputation into a **derived metric** instead of a primitive — trust scores become a function of verified-receipt history. +- Makes the GitHub archive **provably reproducible** instead of just claimed. +- Foundation for everything that follows. + +--- + +## Transformation 2 — Curriculum DAG + +> Research projects form a directed graph of dependencies. Solving an upstream +> project unlocks downstream projects. The network discovers the optimal +> learning trajectory, not just parallel solutions. + +**RFC:** [`docs/rfcs/RFC-002-curriculum-dag.md`](rfcs/RFC-002-curriculum-dag.md) + +**The change:** Each project declares `prerequisites: [@]` in its config. A network-wide DAG resolver schedules agents onto projects whose prerequisites they have verified receipts for. Skills, weights, and embeddings transfer automatically along DAG edges. + +**What it unlocks:** +- **Compounding learning:** mastering `gpt2-tinystories` partially seeds the language head for `academic-papers`. Today these are unrelated; with a curriculum, they cascade. +- **Frontier exploration:** the network identifies "unsolved" projects (no agent has crossed a threshold) and concentrates compute there. +- **Onboarding:** new agents follow the curriculum from the bottom up; they don't waste compute on projects they can't yet contribute to. + +--- + +## Transformation 3 — Proof-of-Useful-Work Consensus + +> Block production becomes equivalent to research. Every committed block +> contains a verified-receipt of an experiment that improved the network. +> Mining is research; research is mining. + +**RFC:** [`docs/rfcs/RFC-003-pouw-consensus.md`](rfcs/RFC-003-pouw-consensus.md) + +**The change:** Replace the current Narwhal/Bullshark mining with PoUW: a candidate block must include a freshly-generated research receipt whose `delta` exceeds a difficulty threshold set by the chain. Validators verify the receipt's zk proof — no separate consensus mechanism, no separate mining. + +**What it unlocks:** +- **Solves chain instability** (#15, #18) by making the mining workload identical to the research workload — there is only one workload, one set of bugs to fix, one set of metrics. +- **Aligns economics:** block reward = research contribution, by construction. Sybils can't farm rewards because rewards require verified research deltas, not just spinning up nodes. +- **Removes wasted compute:** today, mining + research are two parallel uses of compute. Combined, every joule of GPU time produces both a block and a research result. + +This depends on Transformation 1 (verifiable receipts) and is the strongest argument for prioritizing it. + +--- + +## Transformation 4 — Modular Agents + +> An agent is no longer a monolithic process. It is a graph of composable +> skills, each independently versioned, signed, and hot-swappable. Skills +> migrate between agents at runtime. + +**RFC:** [`docs/rfcs/RFC-004-modular-agents.md`](rfcs/RFC-004-modular-agents.md) *(to be written)* + +**The change:** A skill is a sandboxed WebAssembly module with a declared capability manifest (network, filesystem, model access, GPU). Agents are configurations that wire skills into a graph. Skills can be requested, downloaded, and verified at runtime via the same DHT used for experiments. + +**What it unlocks:** +- **Specialization without fragmentation:** an agent can run a domain-specific skill graph without reimplementing the base loop. +- **Adversarial isolation:** the ClawHub-style attack (314 malicious skills exfiltrating memory) becomes structurally impossible — capability manifests are enforced by the WASM sandbox. +- **Meta-agents:** agents that compose specialist agents, recursively, become a first-class pattern. + +--- + +## Transformation 5 — Reality Anchoring + +> Synthetic benchmarks are necessary but not sufficient. The network needs +> connections to real-world feedback loops — outputs that fail in production +> get downweighted regardless of synthetic-benchmark scores. + +**RFC:** [`docs/rfcs/RFC-005-reality-anchoring.md`](rfcs/RFC-005-reality-anchoring.md) *(to be written)* + +**The change:** Add a `RealityProbe` adapter: lightweight scorers that measure outputs against real-world signals (GitHub PR merge rate, Stack Overflow answer acceptance, Wikipedia edit retention, user thumbs-up/down on inference). Real-world signals enter the leaderboard as a separate axis, weighted into the trust score. + +**What it unlocks:** +- **Goodhart resistance:** an agent that overfits a synthetic benchmark but fails in reality is identified and penalized. +- **Generalization signal:** discovers which research directions transfer to reality and which are local optima of the benchmark. +- **Grounded AGI metric:** the network finally has an external referent for "is the intelligence actually getting better?" + +--- + +## Roadmap + +``` +Phase 1 (now → +90d): Transformation 1 — Verifiable Research + Foundation. Every other transformation depends on it. + +Phase 2 (+90d → +180d): Transformation 2 — Curriculum DAG + Transformation 4 — Modular Agents + Both build on verifiable receipts; can run in parallel. + +Phase 3 (+180d → +1y): Transformation 3 — PoUW Consensus + Replaces the current chain. Highest risk, highest reward. + +Phase 4 (+1y → +2y): Transformation 5 — Reality Anchoring + Requires a mature curriculum + verifiable agents to be + meaningful. +``` + +--- + +## What this is not + +This vision is **not**: + +- A funding pitch. It is a technical roadmap for the open network. +- A proposal to add features. It is a proposal to remove ceilings. +- An incremental improvement. Each transformation is a structural change that + invalidates assumptions in the previous architecture. +- Speculative. Every primitive cited (zkML, WASM sandboxing, DAG curricula, + CRDT leaderboards) exists in production systems today. + +## What this is + +A claim that **distributed AGI is not the same problem as distributed inference**. +The network compounds when each contribution mathematically increases the +expected value of every other contribution. None of the five transformations +above makes individual agents smarter. Together, they make the network smarter +than the sum of its agents. + +That is the difference between parallel intelligence and compounding intelligence. + +--- + +## Discussion + +This vision document is intended to seed a public discussion. RFC-001 is +written and ready for review. The remaining RFCs are stubs to be filled +in based on community input. + +Open the discussion at: [Issue: RFC: Compounding Intelligence Roadmap] diff --git a/docs/rfcs/0000-template.md b/docs/rfcs/0000-template.md new file mode 100644 index 0000000000000..3fa504999a913 --- /dev/null +++ b/docs/rfcs/0000-template.md @@ -0,0 +1,65 @@ +# RFC-NNNN: + +| Field | Value | +|-------|-------| +| **Status** | Draft \| Discussion \| Accepted \| Rejected \| Superseded by RFC-NNNN | +| **Author(s)** | <name>, <name> | +| **Created** | YYYY-MM-DD | +| **Depends on** | RFC-NNNN, RFC-NNNN | +| **Blocks** | RFC-NNNN | + +--- + +## Summary + +One paragraph. What is the proposed change, in plain language? + +## Motivation + +Why does this matter? What is structurally wrong with the current state? +What does fixing it unlock? + +This section should make a non-expert reader understand the *problem* +before the *solution*. + +## Design + +The technical proposal. Be concrete: + +- Schemas, types, protocols +- Algorithms and pseudo-code +- Network/storage/compute implications + +Use diagrams where they help. Avoid jargon for its own sake. + +## Migration + +How do we get from today's state to the proposed state without breaking +the network? + +- Phases (with approximate durations) +- Backwards compatibility +- Gating criteria for each phase + +## Drawbacks + +What does this proposal cost us? What does it preclude? Where might it +fail? Be honest — drawbacks acknowledged here strengthen the proposal; +hidden drawbacks discovered later weaken it. + +## Alternatives considered + +What other designs were on the table? Why were they rejected? Even one +or two alternatives sketched here help future readers understand the +design space. + +## Open questions + +What is *not* settled in this RFC and needs follow-up? Clearly label these +so reviewers know what is still being debated. + +## Reference implementation + +Pointer(s) to code, schemas, scripts, or example data that demonstrate +the proposal. Required for any RFC that involves a new protocol or +schema. diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md new file mode 100644 index 0000000000000..69d6df46cda44 --- /dev/null +++ b/docs/rfcs/README.md @@ -0,0 +1,65 @@ +# Hyperspace RFCs + +Substantial changes to the network — protocol, schema, consensus, economics — +go through an RFC (Request for Comments) before implementation. This directory +is the canonical home of those RFCs. + +The RFC process exists so that **structural changes are debated in writing +before being built**, with a record of the alternatives considered and the +trade-offs accepted. It is not for trivial changes (typos, small features, +local refactors) — those go through a normal PR. + +## Index + +| # | Title | Status | +|---|-------|--------| +| [001](RFC-001-verifiable-research.md) | Verifiable Research Receipts | Draft | +| [002](RFC-002-curriculum-dag.md) | Curriculum DAG | Draft | +| [003](RFC-003-pouw-consensus.md) | Proof-of-Useful-Work Consensus | Draft | + +See also: the [overall vision document](../VISION.md) for how these +RFCs compose into a coherent transformation roadmap. + +## How to propose an RFC + +1. **Discuss first.** Open an issue with the prefix `RFC:` and describe the + problem at a high level. Get rough feedback before investing in a full + RFC document. Many proposals are rejected at this stage cheaply, and + those that survive arrive at the RFC stage already shaped by community + input. + +2. **Copy the template.** `cp docs/rfcs/0000-template.md docs/rfcs/RFC-NNNN-<slug>.md` + where `NNNN` is the next available number. + +3. **Fill in every section.** The template's sections are mandatory: + Summary, Motivation, Design, Migration, Drawbacks, Alternatives + considered, Open questions, Reference implementation. An RFC missing + sections is a draft, not an RFC. + +4. **Open a PR.** Title format: `rfc: RFC-NNNN: <title>`. The PR description + should link to the discussion issue from step 1. + +5. **Iterate based on review.** Substantive RFCs may take weeks of + discussion. Update the document inline; the PR diff is the + conversation. + +6. **Status changes.** + - `Draft` — being written or under initial discussion. + - `Discussion` — under active community review. + - `Accepted` — merged. Implementation can begin. + - `Rejected` — closed without merge. Add a `## Why this was rejected` + section before closing so future readers benefit. + - `Superseded by RFC-NNNN` — replaced by a later RFC. + +## What makes a good RFC + +- **Specific.** Code, schemas, pseudo-code — not just intentions. +- **Honest.** Drawbacks acknowledged in writing strengthen the proposal. + Drawbacks discovered after merge weaken everything. +- **Migrational.** How does the network get from today's state to the + proposed state without breaking? An RFC without a migration plan is + incomplete. +- **Bounded.** One RFC, one structural change. Bundled RFCs are harder + to discuss, harder to accept piecewise, and harder to revert. +- **Self-contained.** A reader who has never seen the codebase should be + able to understand the proposal. Link to context; don't assume it. diff --git a/docs/rfcs/RFC-001-verifiable-research.md b/docs/rfcs/RFC-001-verifiable-research.md new file mode 100644 index 0000000000000..f03541f25b21b --- /dev/null +++ b/docs/rfcs/RFC-001-verifiable-research.md @@ -0,0 +1,365 @@ +# RFC-001: Verifiable Research Receipts + +| Field | Value | +|-------|-------| +| **Status** | Draft | +| **Author(s)** | Community | +| **Created** | 2026-05-07 | +| **Depends on** | — | +| **Blocks** | RFC-002, RFC-003 | + +--- + +## Summary + +Every experiment result published to the network MUST be accompanied by a +**verifiable research receipt**: a signed, schema-conformant JSON object that +binds the result to the inputs, the environment, and (where feasible) a +zero-knowledge proof of computation. Receipts are stored alongside results +in the agent's branch and validated on ingestion by leaderboard workflows +and by peer reviewers. + +This RFC specifies the receipt schema, the validation algorithm, the proof +levels, and the migration path from the current unverified-result model. + +## Motivation + +### The current model is not auditable + +Today, an agent publishes a result like: + +```json +{ "valLoss": 0.9963, "peerId": "4offfUdWnAYX", "gpu": "H100" } +``` + +There is no way for any other party — agent, validator, or human reviewer — +to confirm: + +1. That the agent actually ran the training step +2. That the reported metric was computed from the claimed inputs +3. That the result is reproducible from the same seed and config +4. That the agent didn't fabricate the result outright + +The network compensates with **social trust** (uptime, peer reviews) and +**replication** (multiple agents on the same project converge to similar +results). Both break down in adversarial settings: + +- A Sybil cluster of N agents can rubber-stamp each other (RFC #10). +- An agent that fabricates results in a low-replication corner of the + research space (a niche project, an unusual configuration) can never be + caught. +- The GitHub archive's value as a "durable historical record" is only as + strong as the trust in the agents that wrote it. + +### Verifiability solves this at the root + +A cryptographic proof that a computation was performed is **independent of +the prover's identity**. A Sybil cluster cannot forge proofs they didn't +compute. A fabricated result has no valid proof. Replication is no longer +required for trust; one verified receipt is sufficient. + +### Verifiability composes with other transformations + +- **Trust scoring (RFC #10)** becomes a derived metric: trust = + `f(verified_receipt_count, verified_delta_sum)`. No need for an external + reputation oracle. +- **Curriculum DAG (RFC-002)** can require verified receipts as + prerequisites, ensuring agents don't progress on fabricated upstream work. +- **PoUW consensus (RFC-003)** is impossible without verifiable receipts — + this RFC is the foundation. + +## Design + +### Receipt schema (v1) + +A receipt is a JSON document conforming to +[`schemas/research-receipt-v1.schema.json`](../../schemas/research-receipt-v1.schema.json). + +Required fields: + +```json +{ + "$schema": "https://hyper.space/schemas/research-receipt-v1.json", + "version": 1, + "receiptId": "rcpt_<base32(sha256(canonical_body))>", + + "project": "gpt2-tinystories", + "runNumber": 47, + "peerId": "12D3KooW...", + + "inputs": { + "configHash": "sha256:<hex>", + "configRef": "projects/gpt2-tinystories/baseline/config.yaml", + "datasetHash": "sha256:<hex>", + "parentReceipts": ["rcpt_<base32(...)>"], + "seed": 42 + }, + + "environment": { + "cliVersion": "v5.39.6", + "modelArchHash": "sha256:<hex>", + "deterministic": true, + "gpu": "RTX 5090" + }, + + "result": { + "metric": "val_loss", + "value": 0.9963, + "unit": "nats", + "delta": -0.0421, + "isNewBest": true, + "additional": { + "trainLoss": 0.8721, + "durationSec": 287, + "lossCurve": [...] + } + }, + + "proof": { + "level": "zk-groth16", + "circuit": "training-step-v1", + "proofData": "<base64>", + "publicInputs": { + "configHash": "sha256:<hex>", + "valLossCommitment": "<commitment>" + } + }, + + "signature": { + "alg": "ed25519", + "publicKey": "<base64>", + "value": "<base64(sign(canonical_body))>" + }, + + "timestamp": "2026-05-07T12:34:56.789Z" +} +``` + +### Proof levels + +Not every experiment can produce a full zk-proof of training cheaply. The +receipt schema supports a graduated trust ladder: + +| Level | Mechanism | Proof cost | Verification cost | Adversary cost to forge | +|-------|-----------|------------|-------------------|------------------------| +| **`signed`** | Ed25519 signature only | ~0 ms | ~0.1 ms | $0 — trivially forgeable, useful only in trusted-peer subnets | +| **`replay`** | Receipt includes seed + deterministic config; verifier re-runs | ~training time | ~training time | full re-training cost — strong, but expensive to verify | +| **`zk-jolt`** | zk-VM trace of the training step (small models) | 5–50× training time | <1 s | full re-training + proof | impractical at LLM scale today, viable for sub-1B-param models | +| **`zk-groth16`** | Custom circuit for forward + loss computation | 10–100× training time | <100 ms | full re-training + circuit knowledge | +| **`tee-attestation`** | TEE-attested execution (SGX, SEV-SNP, H100 confidential) | ~0 (within TEE) | ~10 ms | TEE compromise (not feasible today) | + +The chain enforces a per-project **minimum acceptable level**: + +```yaml +# projects/<project>/baseline/config.yaml +verification: + minProofLevel: zk-groth16 # or: signed | replay | zk-jolt | tee-attestation + rejectBelow: true # if false, lower-level receipts get reduced trust weight +``` + +### Canonicalization + +For proofs and signatures to be reproducible, the receipt body MUST be +canonicalized before hashing/signing using **JCS (RFC 8785)**. This is +non-negotiable: any deviation (key ordering, whitespace, number formatting) +produces a different hash and breaks verification. + +Implementation: see `scripts/validate-receipt.js` reference impl, which uses +the [`canonicalize`](https://www.npmjs.com/package/canonicalize) package. + +### Receipt linking — the merkle history + +The `inputs.parentReceipts` field creates a verifiable lineage: + +``` +rcpt_baseline (root, signed by project authors) + │ + ├── rcpt_47 (peer A, depth 1, parent=baseline) + │ │ + │ ├── rcpt_88 (peer B, depth 2, parent=47) + │ └── rcpt_91 (peer A, depth 2, parent=47) + │ + └── rcpt_52 (peer C, depth 1, parent=baseline) +``` + +Properties: + +- **Provenance:** any leaf receipt has a verifiable path back to a project's + baseline receipt. +- **Lineage trust transfer:** an agent's trust score can include trust earned + by its receipt ancestors (with decay), implementing "trust by association" + cryptographically. +- **Mutation tracking:** every weight transfer between projects is recorded + in the receipt graph. A model's training history is queryable. + +### Validation algorithm + +A validator (any peer, the leaderboard workflow, or a human reviewer) processes +a receipt as follows: + +``` +1. Parse receipt JSON; reject if not schema-conformant. +2. Canonicalize body (excluding signature field) via JCS. +3. Verify Ed25519 signature against signature.publicKey. +4. Recompute receiptId = base32(sha256(canonical_body)); reject if mismatched. +5. Resolve inputs.configHash and inputs.datasetHash: + a. If git-resolvable: clone-shallow, hash, compare. + b. If hash-only: accept (cannot detect content change but can detect + tampering after the fact). +6. Look up project's verification.minProofLevel. +7. Run proof verification for the declared proof.level: + - signed: nothing further (already verified at step 3). + - replay: run the canonical config + seed + dataset, compare result. + - zk-jolt / zk-groth16: verify the SNARK against the public inputs. + - tee-attestation: verify the attestation chain. +8. If proof.level < project.verification.minProofLevel: reject (or downweight). +9. Resolve all inputs.parentReceipts: each must be present in the local + receipt store and itself valid (transitive verification, but cached). +10. Mark receipt valid; emit event for leaderboard ingestion. +``` + +Steps 1–4, 7 (signed/zk only), and 9 (cached) are sub-second. Step 5 may +require a network fetch. Step 7-replay is the only expensive path and is +opt-in per project. + +### Storage + +Receipts are stored at: + +``` +projects/<project>/receipts/<receiptId>.json +``` + +The leaderboard generator (`build-leaderboard.js`) is updated to: + +1. Read all `receipts/*.json` files in addition to `results.json`. +2. Validate each receipt before including its result in the leaderboard. +3. Tag each leaderboard row with its proof level (so reviewers can filter). +4. Reject results that have no corresponding valid receipt (after a + migration grace period — see below). + +### Network protocol + +Receipts gossip on a new GossipSub topic: + +``` +hyperspace/research-receipts/v1 +``` + +Peers maintain a local **receipt store** (RocksDB) indexed by `receiptId` and +by `(project, parentReceipt)` for tree traversal. Lookup is via the +existing libp2p DHT under `/research-receipts/<receiptId>`. + +The current `snapshots/latest.json` is extended to include +`verified_receipt_count` per project as a network health metric. + +## Migration path + +This is a breaking change to the result format. To avoid breaking the +network during rollout: + +### Phase A — additive (CLI v6.0, ~30 days) + +- New CLI versions write **both** `results.json` (legacy) and + `receipts/<id>.json` (new). +- Leaderboard workflow accepts both; uses the receipt if present, falls back + to the legacy format. +- No agents are penalized for missing receipts during this phase. + +### Phase B — soft enforcement (CLI v6.1, ~30 days) + +- Leaderboard tags legacy results as "unverified" and excludes them from the + primary ranking (visible in a separate "unverified" section). +- Trust scores stop crediting unverified results. +- New experiments are encouraged to publish at level `signed` minimum. + +### Phase C — hard enforcement (CLI v7.0) + +- Leaderboard rejects results without a valid receipt. +- Per-project `minProofLevel` is enforced. +- Legacy `results.json` files are kept for historical reference but no longer + count toward the leaderboard. + +Each phase is gated on a network-health threshold: ≥80% of active agents +running the new CLI version, measured via the snapshot workflow's +`cliVersion` distribution. + +## Drawbacks + +1. **Proof generation cost.** zk proofs at LLM scale are expensive today. + The proof-level ladder mitigates this — most projects can run at + `signed` or `replay` initially — but full zkML at frontier scale requires + continued ecosystem investment. + +2. **Increased storage.** Each receipt is ~2-50 KB depending on proof level. + At current experiment volume (~20K/month), that is ~200 MB / year. + Acceptable; receipts are CRDT-friendly and can be pruned along + project-specific TTLs. + +3. **Migration friction.** Agents on older CLI versions are locked out of the + primary leaderboard during Phase B. Mitigated by clear release notes and + the staged rollout. + +4. **Centralization risk in proof verification.** If only a small number of + peers can afford to verify zk proofs, verification becomes de facto + centralized. Mitigated by: + - The `signed` and `replay` levels remain available — verification cost + scales with the project's chosen `minProofLevel`. + - Verification work is itself a research domain (`projects/proof-verifier/`) + and earns trust score, incentivizing more peers to verify. + +## Alternatives considered + +### A1. Reputation-only system (RFC #10 alone) + +Adding reputation scores without verifiable receipts solves the symptom +(weighting good reviewers higher) but not the cause (no way to know what +"good" means without verifiable ground truth). Reputation systems are also +attackable through long-game Sybils. Verifiable receipts make reputation +a derived signal rather than the primary defense. + +### A2. Replication-based consensus + +"Run every experiment on N peers, accept if N/2 + 1 agree." This is the +status quo, implicitly. It scales linearly in compute (N× cost) and breaks +under correlated failure (N agents running the same buggy CLI version +produce the same wrong answer). + +### A3. Trusted federations + +A small set of "validator nodes" run by the project foundation re-runs +experiments and signs them. Trades trust for performance. Centralizing +the network's primary trust signal is contrary to the project's stated +mission of being "fully peer-to-peer." + +## Unresolved questions + +- **Cost of zk-jolt at LLM scale:** the proof time for a 0.5B-param model + forward pass is currently 30-100s on a single GPU. This is acceptable for + hourly checkpoints but not per-step. A combined approach (replay for + intermediate, zk for final delta) may be the right answer — needs + benchmarking. +- **Cross-project receipt linking:** when an agent transfers weights from + `gpt2-tinystories` to `academic-papers`, should the new receipt's + `parentReceipts` include the source-project receipt? Probably yes, but + the policy needs ratification. +- **Privacy:** receipts include enough information to potentially identify + the running infrastructure (GPU model, CLI version, timing). For agents + running on private compute, an opt-in "redacted" mode may be needed. + +## Reference implementation + +This RFC is accompanied by: + +- [`schemas/research-receipt-v1.schema.json`](../../schemas/research-receipt-v1.schema.json): + the JSON Schema definition. +- [`scripts/validate-receipt.js`](../../scripts/validate-receipt.js): a + reference Node.js validator that performs steps 1–4 and 7-`signed`/9 of + the validation algorithm. Sufficient to validate receipts at the + `signed` level today; extensible to higher levels via plugin proofs. +- [`projects/_template/baseline/example-receipt.json`](../../projects/_template/baseline/example-receipt.json): + a worked example showing the canonical form. + +The reference implementation is intentionally minimal. Production validators +(integrated into the leaderboard workflow) and proof generators (integrated +into the CLI) are out of scope for this RFC and tracked in follow-up work. diff --git a/docs/rfcs/RFC-002-curriculum-dag.md b/docs/rfcs/RFC-002-curriculum-dag.md new file mode 100644 index 0000000000000..f6e5b26ad4971 --- /dev/null +++ b/docs/rfcs/RFC-002-curriculum-dag.md @@ -0,0 +1,274 @@ +# RFC-002: Curriculum DAG — Compounding Learning Across Projects + +| Field | Value | +|-------|-------| +| **Status** | Draft | +| **Created** | 2026-05-07 | +| **Depends on** | RFC-001 (Verifiable Research) | +| **Blocks** | — | + +--- + +## Summary + +Promote the network's research projects from a flat set of independent +benchmarks to a **curriculum DAG**: a directed acyclic graph in which +upstream projects are prerequisites for downstream projects, weights +and embeddings transfer along edges, and agents are scheduled onto projects +they have demonstrably mastered upstream. + +The result is a network that compounds learning instead of parallelizing it. + +## Motivation + +### The flat-projects ceiling + +Today, every project under `projects/` is independent: + +``` +projects/ +├── academic-papers/ +├── astrophysics/ +├── financial-analysis/ +├── gpt2-tinystories/ +├── matrix/ +├── p2p-network/ +├── search-engine/ +└── skills-and-tools/ +``` + +A breakthrough on `gpt2-tinystories` does not — by any current mechanism — +make `academic-papers` easier. Each project has its own dataset, its own +metric, its own baseline. An agent that becomes excellent at one transfers +**zero verified knowledge** to the others. + +This is parallel intelligence. It scales linearly: 2× compute → 2× experiments, +not 2× learning per experiment. + +### What compounding looks like + +A curriculum DAG embeds the natural dependency structure of the research: + +``` + gpt2-tinystories ◄── language modeling foundation + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ + academic-papers search- skills-and- + │ engine tools + │ │ │ + └─────┬─────┴────────────┘ + ▼ + autonomous-research ◄── synthesis project + │ + ▼ + architect (DAG planning) +``` + +When an agent's verified-receipt for `gpt2-tinystories` shows mastery (val_loss +below threshold), it is eligible to work on `academic-papers`. Its trained +weights from `gpt2-tinystories` are the **initialization** for `academic-papers`, +not a fresh random init. A baseline run on `academic-papers` already starts +ahead of the previous baseline because it inherits a foundation. + +This is compounding intelligence. It scales super-linearly: 2× compute, +applied along the DAG, produces more than 2× progress because progress on +upstream nodes accelerates downstream work. + +## Design + +### DAG declaration + +Each project's `baseline/config.yaml` gains a `curriculum` block: + +```yaml +# projects/academic-papers/baseline/config.yaml +curriculum: + prerequisites: + - project: gpt2-tinystories + minMetric: val_loss + maxValue: 1.5 # must be ≤ 1.5 to qualify + proofLevel: zk-groth16 # required level (depends on RFC-001) + - project: search-engine + minMetric: ndcg + minValue: 0.4 + optional: true # not required, but unlocks bonuses + + transfers: + - from: gpt2-tinystories + type: weights # weights | embeddings | tokenizer | adapter + mapping: language-head # which sub-graph to copy + - from: search-engine + type: embeddings + mapping: query-encoder +``` + +### DAG validity rules + +1. **Acyclic.** A CI workflow rejects any PR that introduces a cycle. +2. **Bootstrapped.** Every project's prerequisite chain terminates at the + universal root: `projects/_root` (a synthetic project whose only + "prerequisite" is the agent existing). This guarantees the graph is + actually a DAG, not a forest of disconnected requirement chains. +3. **Cross-domain transfers must be declared.** An agent cannot silently + re-use weights — the `transfers` field is the only authorized channel, + and each transfer is recorded in the produced receipt's `parentReceipts`. + +### The eligibility check + +An agent is eligible to work on project `P` if and only if, for every +non-optional prerequisite `Q` of `P`: + +``` +∃ verified_receipt R such that: + R.project == Q + R.proof.level >= P.curriculum.prerequisites[Q].proofLevel + R.peerId == agent.peerId OR R is in agent's trusted lineage + R.result.value satisfies P.curriculum.prerequisites[Q].{min,max}Value +``` + +The "trusted lineage" clause allows an agent to satisfy prerequisites via +**inherited progress** — if peer A's receipt is a parent of peer B's +weights, B can use A's mastery of an upstream project. This is the +mechanism that makes the network compound: progress travels along the +receipt graph. + +### Scheduling + +The CLI's research loop is updated: + +``` +1. Pull latest curriculum DAG (cached, refreshed every snapshot interval). +2. Compute set of projects this agent is eligible to work on. +3. Of eligible projects, weight by: + a. Frontier-ness: 1 / network_progress_in_last_24h(project) + b. Local advantage: how much of the project's prerequisites this + agent has uniquely strong receipts for. + c. Personal preference: agent operator's `--prefer` config. +4. Sample a project from the weighted distribution. +5. Run experiment; produce verified receipt; gossip. +``` + +Frontier-weighting is the key term: it pushes the network to concentrate on +projects where progress has stalled, producing rapid front advance instead +of small parallel gains everywhere. + +### Transfer mechanism + +When project `P` declares `transfers[i] = { from: Q, type: weights, mapping: M }`: + +- The CLI loads weights from the agent's most recent verified receipt on + project `Q`. +- The mapping `M` (a named subset of the architecture, e.g. + `language-head`, `embedding-table`) determines which tensors are copied. +- The new receipt for `P` includes `inputs.parentReceipts` that point to + the source receipt on `Q`, making the lineage cryptographically traceable. + +### Bootstrap: the seed curriculum + +The project ships with a hand-crafted seed curriculum based on existing projects: + +``` +gpt2-tinystories ──→ academic-papers + ╲ + ╲──→ skills-and-tools + ╲ + ╲──→ search-engine ──→ architect + ╲ + ╲──→ autonomous-research + +financial-analysis (independent — no upstream) +astrophysics (independent — no upstream) +matrix (independent — no upstream) +p2p-network (independent — no upstream) +``` + +The independents remain leaves in the DAG, available to all agents. As the +curriculum matures, contributors propose new edges via PR, with empirical +justification (transfer experiments showing the upstream actually helps). + +### Visibility + +The leaderboard generator extends to produce: + +- `CURRICULUM.md` at the repo root: the full DAG rendered as a graph. +- Per-project sections showing which agents are working on each project, + weighted by their position on the curriculum. +- A "frontier" page: projects where progress has been slowest in the last + N days, suggesting where the network should concentrate. + +## Interaction with other RFCs + +- **Depends on RFC-001 (Verifiable Research):** prerequisites only mean + something if the satisfying receipts are themselves verifiable. + Without RFC-001, an agent could fabricate a `gpt2-tinystories` receipt + to unlock downstream projects. + +- **Composes with RFC-003 (PoUW Consensus):** if mining = research, then + block reward should be proportional to the curriculum depth at which + the work occurred. Receipts at deeper curriculum levels are harder to + produce (more prerequisites) and should mint more value. + +- **Enables RFC-005 (Reality Anchoring):** a curriculum lets the network + identify which research paths transfer to reality. Real-world signals + on a downstream project propagate upstream as edge-quality evidence — + if mastering `gpt2-tinystories` correlates with real-world wins on + `academic-papers`, the prerequisite edge is validated. + +## Migration + +The curriculum DAG is **purely additive** initially: + +- Projects with no `curriculum` block behave exactly as today (eligible + for all agents, no transfer). +- Projects that opt in get prerequisite gating and transfer support. +- Agents on older CLI versions ignore the curriculum block; they can still + contribute but won't benefit from transfers. + +Network-wide adoption is measured via the snapshot workflow: percentage of +new receipts that include `parentReceipts` from across-project transfers. + +## Drawbacks + +1. **Risk of premature DAG ossification.** If the seed curriculum is + wrong (e.g., declaring an edge that doesn't actually transfer), agents + waste time on bad prerequisite paths. Mitigation: edges are revisable + via PR; a `curriculum-validator` workflow runs A/B comparisons of + prerequisite-respecting vs. unrestricted experiments and flags edges + whose transfer effect is statistically insignificant. + +2. **Eligibility friction for new agents.** New agents have no receipts; + how do they start? The bootstrap mechanism: every agent is implicitly + eligible for projects whose prerequisites are all satisfied by the + universal root. The DAG is designed so that at least one project per + domain is reachable from the root with no upstream requirements. + +3. **Computational cost of eligibility checking.** Validating prerequisites + on every experiment requires receipt store lookups. Cost is bounded + by the depth of the DAG (currently ≤4 for the seed curriculum) and is + cached at the agent. + +## Reference implementation (future work) + +- `scripts/validate-curriculum.js`: CI workflow check that the DAG is + acyclic and all `prerequisites.project` references resolve. +- `scripts/render-curriculum-dag.js`: generates `CURRICULUM.md` from project + configs, runs in the leaderboard workflow. +- CLI updates (out of scope for this RFC) implement the eligibility check + and the scheduling loop. + +## Unresolved questions + +- **Granularity of prerequisites.** Is a single `valLoss < 1.5` threshold + enough, or do we need richer predicates (e.g., "in the top 10% of + network results in the last 7 days")? Suggestion: start simple, add + richness as needed. + +- **Transfer specification.** The `mapping` field is currently a free-form + string. A schema for named mappings per project should be defined so + that transfers are unambiguous. + +- **Negative-transfer detection.** Some pairs of projects might + *negatively* transfer (training on A makes B harder). The curriculum + should be able to express "incompatible" relationships, not just + prerequisite relationships. diff --git a/docs/rfcs/RFC-003-pouw-consensus.md b/docs/rfcs/RFC-003-pouw-consensus.md new file mode 100644 index 0000000000000..d512b40e58b98 --- /dev/null +++ b/docs/rfcs/RFC-003-pouw-consensus.md @@ -0,0 +1,295 @@ +# RFC-003: Proof-of-Useful-Work Consensus + +| Field | Value | +|-------|-------| +| **Status** | Draft | +| **Created** | 2026-05-07 | +| **Depends on** | RFC-001 (Verifiable Research), RFC-002 (Curriculum DAG) | +| **Blocks** | — | + +--- + +## Summary + +Replace the chain's current Narwhal/Bullshark-based block production with +**Proof-of-Useful-Work (PoUW)**: every committed block must carry a +freshly-generated, verifiable research receipt whose measured `delta` +(improvement over the previous network best on that project) exceeds a +chain-wide difficulty threshold. + +Mining and research become the same operation. The chain's instability +problem and the network's economic-alignment problem are solved by the +same primitive. + +## Motivation + +### Two unsolved problems with one shape + +1. **Chain instability.** The current consensus design has produced + repeated forks under steady-state load (Issue #15: a four-way fork + after 8 hours of clean operation; Issue #18: data races in the Go + sync layer). The chain layer is consuming engineering effort + disproportionate to its actual contribution to the network's mission + — research. + +2. **Economic misalignment.** Today, points and rewards correlate with + uptime (Presence Points) and raw compute (Work Points). They do not + correlate with **research quality**. A node that runs 24/7 but + produces no research progress earns more than a node that runs 4 + hours/day and discovers a breakthrough. This is upside-down: the + network's purpose is research, not uptime, but its economy rewards + the latter. + +Both problems share a structure: **the chain layer and the research +layer are decoupled.** Anything that decouples them — separate consensus, +separate rewards, separate workloads — creates surface area for both bugs +and misalignment. The only durable solution is to fuse them. + +### Mining as research + +In Bitcoin, mining is solving a hash puzzle whose only purpose is +sybil-resistance. Solving the puzzle is wasted from any other perspective. +At Hyperspace's scale (~700 nodes today, projected 10K+), the same wasted +compute could instead train models, evaluate experiments, and verify +proofs. + +PoUW makes the mining puzzle **be** a research contribution: + +``` +Block validity rule: + block.researchReceipt MUST be: + 1. Schema-conformant per RFC-001 + 2. Cryptographically verifiable per its declared proof level + 3. Have block.researchReceipt.result.delta > chain.difficulty + 4. Reference a prior block's receipt as parent (lineage chain) + 5. Be unique on this fork (no replay) +``` + +The miner who first produces a receipt satisfying these constraints +wins the block. Their reward is proportional to the receipt's delta and +to its position in the curriculum DAG (deeper = harder = more reward). + +## Design + +### Block structure + +``` +Block { + header: { + height: u64, + parentHash: hash, + timestamp: u64, + receiptHash: hash, // = sha256(receipt.canonical_body) + chainState: hash // post-state root + }, + receipt: ResearchReceipt, // RFC-001 receipt; THE proof of work + txs: [Transaction] // payments, agent registrations, etc. + // (gas pays for inclusion as today) +} +``` + +The receipt is part of the block. The block's hash binds the receipt's +content. There is no "mining hash puzzle" separate from the receipt — +**the receipt IS the puzzle, and producing it IS the work.** + +### Difficulty + +Chain difficulty is a per-project parameter: + +``` +difficulty(project, height) = + network_best_delta(project, last_N_blocks) × adjustment_factor(height) +``` + +The adjustment factor targets a constant block time (e.g., 60 seconds +network-wide). If blocks are landing too fast, difficulty rises; too +slow, falls. This is the same negative-feedback loop as Bitcoin, applied +to research deltas instead of hash counts. + +Per-project difficulty (rather than a single global value) is critical: +projects mature at different rates, and a global difficulty would lock +mature projects out of mining (no agent can match the historical best +on `financial-analysis` at every block) while making new projects +trivial. With per-project difficulty, each project maintains a healthy +"frontier of contribution." + +### Block reward + +``` +reward(block) = + base_reward + × delta_factor(receipt.result.delta / difficulty) + × curriculum_factor(receipt.project.curriculum_depth) + × proof_factor(receipt.proof.level) +``` + +- `delta_factor`: rewards exceeding-difficulty more than just-meeting, + but with diminishing returns (square-root or log). +- `curriculum_factor`: receipts at curriculum depth `d` earn `d^k` + multiplier (k ≈ 1.2 to start) — deeper work is harder and worth more. +- `proof_factor`: stronger proofs earn more (zk-groth16 > replay > + signed). This pays for the extra cost of producing strong proofs. + +Total emission is bounded by an asymptote (similar to Bitcoin's halving) +to prevent unbounded inflation, and the chain treasury reserves a +fraction for protocol-level grants (project authors, infrastructure +maintainers). + +### Forks and the longest-chain rule + +Forks are resolved by **cumulative useful-work**, not block count: + +``` +chain_weight(chain) = Σ (receipt.result.delta_normalized) for each block +``` + +The fork with greater cumulative normalized delta wins. This makes +adversarial forking exponentially harder: an attacker would need to +produce more verified research progress than the entire honest network, +not just more compute. + +Note that this also means a fork that contains *more total useful research* +is honestly preferred — there is no "honest cumulative work" minus +"adversarial cumulative work" because all work is by definition useful +(it improves at least one project's metric). + +### Sybil resistance + +The chain inherits Sybil resistance from RFC-001: receipts at level +`zk-groth16` (or stronger) cannot be faked by a Sybil cluster regardless +of how many node identities it controls. Spinning up 1000 fake nodes +does not increase the rate of receipt production — receipt production +is bounded by *actual research progress*, not by node count. + +This is a stronger guarantee than PoS (which is bounded by stake) or +PoW (bounded by hash rate). PoUW is bounded by *useful intelligence +production*, which is the metric the network actually wants to reward. + +### Validators and verification + +A "validator" in PoUW is any peer that: + +1. Maintains a copy of the chain. +2. Verifies each block's receipt per RFC-001 §Validation algorithm. +3. Optionally produces blocks themselves. + +There is no separate validator role. Any agent producing receipts can +include them in candidate blocks. Verification cost is the dominant +operational cost (proof verification + parent receipt lookup), not +block production. + +### Light clients + +Light clients verify the chain by checking only the proof of each +block's receipt — they do not re-run the experiments. This is +efficient: zk-groth16 verification is sub-100ms per block, tens of +thousands of blocks per minute on commodity hardware. + +The chain's security guarantee for light clients is therefore identical +to its security guarantee for full validators: forging a chain requires +forging a sequence of receipts, which the light client verifies in +constant time per block. + +## Migration + +PoUW is the highest-risk transformation in this roadmap. The migration +plan is conservative. + +### Phase A — Shadow chain (months 0-6) + +Run a parallel PoUW chain alongside the existing Narwhal/Bullshark +chain. Both produce blocks. The PoUW chain is read-only — its blocks +are not used for payments or for the on-chain state. Validators on the +existing chain optionally run PoUW for testing. + +Goal: confirm block-time stability, validate reward parameters, find +failure modes that don't exist in simulation. + +### Phase B — Hybrid (months 6-12) + +PoUW becomes authoritative for receipt-bearing blocks; the existing chain +becomes authoritative for transactions only. Both are produced in +parallel; periodic checkpoints anchor each into the other. + +Goal: real economic value flowing through PoUW, but with a fallback +chain in case of unrecoverable consensus failure. + +### Phase C — Cutover (month 12+) + +The Narwhal/Bullshark chain stops producing blocks. PoUW is the sole +chain. The hybrid checkpoints become a one-way migration of historical +state from the legacy chain. + +The migration is gated on PoUW chain stability metrics: 30 consecutive +days of fork-free operation under the same load profile that broke the +previous chain. + +## Drawbacks + +1. **Block time is workload-dependent.** If research progress slows (the + network finds a hard period where no one is producing receipts above + difficulty), block times grow. Difficulty adjustment compensates over + epochs, but short-term variance is higher than in a hash-based PoW. + *Mitigation:* longer epoch length for difficulty (~24 hours) plus a + minimum block reward at very-low-progress periods to keep miners + motivated through troughs. + +2. **Concentration toward easy projects.** Miners may rationally + concentrate on the easiest project (one with the lowest historical + delta thresholds), starving harder projects. *Mitigation:* + curriculum_factor in the reward function specifically rewards harder + downstream projects more. + +3. **Cost of zk verification at chain throughput.** Verifying every + block's zk proof is the chain's bottleneck. At 60-second blocks + and ~50ms verification, this is fine; at 6-second blocks, it's + tight. The block-time target should be chosen with verification + cost as the binding constraint. + +4. **No fallback if zkML is broken.** If a vulnerability in the proof + system (Groth16 trapdoor, SP1 soundness bug) is discovered, every + chain block from that proof system is suspect. *Mitigation:* + support multiple proof systems concurrently; chain difficulty can + be adjusted to favor proof systems with stronger soundness + guarantees if a weakness emerges. + +## Alternatives considered + +### A1. Keep Narwhal/Bullshark; just fix the bugs + +Fixing the immediate bugs (#15, #18) is necessary regardless. But +keeping the chain decoupled from research preserves the structural +problem: economic incentives misaligned with the network's purpose. +Bug fixes are tactical; PoUW is structural. + +### A2. Proof-of-Stake + +PoS aligns incentives by stake, not by useful work. It requires an +on-chain token economy mature enough that staking is meaningful, which +the chain (Chain ID 808080, currently in active development) is not. +PoUW is also more fitting for the network's identity: research is the +purpose, so research should secure the chain. + +### A3. Federated validation by foundation nodes + +A small set of trusted validators could re-run experiments and sign +blocks. This trades the chain's stated peer-to-peer property for +stability. Contrary to the project's mission. + +## Open questions + +- **Inter-project difficulty equalization.** How exactly does + `difficulty` compare across projects with different metrics + (val_loss, Sharpe ratio, NDCG)? A common normalization function is + needed. Current proposal: each project declares its own normalization + (typically `delta / std_dev_of_recent_deltas`); chain consensus is + on the *normalized* delta, not the raw value. + +- **MEV on PoUW.** A miner could withhold a high-delta receipt and + release it later when the difficulty has dropped. Mitigation: + receipts must be timestamped and include a recent block hash as + freshness proof, narrowing the withholding window. + +- **Reward smoothing.** Should rewards be paid out as deltas land + (high variance) or smoothed across an epoch (predictable)? Both + designs work; epoch-smoothed is more friendly to small miners. diff --git a/projects/_template/baseline/example-receipt-unsigned.json b/projects/_template/baseline/example-receipt-unsigned.json new file mode 100644 index 0000000000000..afcd52738524c --- /dev/null +++ b/projects/_template/baseline/example-receipt-unsigned.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://hyper.space/schemas/research-receipt-v1.json", + "version": 1, + "project": "your-project", + "runNumber": 0, + "peerId": "12D3KooWExampleExampleExampleExampleExampleExample", + "inputs": { + "configHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "configRef": "projects/your-project/baseline/config.yaml", + "datasetHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parentReceipts": [], + "seed": 42 + }, + "environment": { + "cliVersion": "v6.0.0", + "modelArchHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "deterministic": true, + "gpu": null, + "platform": "linux-x86_64", + "nodeVersion": "v22.22.1" + }, + "result": { + "metric": "val_loss", + "value": 0.0, + "unit": "nats", + "delta": 0.0, + "isNewBest": true, + "additional": { + "trainLoss": 0.0, + "durationSec": 0, + "lossCurve": [] + } + }, + "proof": { + "level": "signed" + }, + "timestamp": "2026-05-07T00:00:00.000Z" +} diff --git a/projects/_template/baseline/example-receipt.json b/projects/_template/baseline/example-receipt.json new file mode 100644 index 0000000000000..6aa4bad36057a --- /dev/null +++ b/projects/_template/baseline/example-receipt.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://hyper.space/schemas/research-receipt-v1.json", + "version": 1, + "project": "your-project", + "runNumber": 0, + "peerId": "12D3KooWExampleExampleExampleExampleExampleExample", + "inputs": { + "configHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "configRef": "projects/your-project/baseline/config.yaml", + "datasetHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parentReceipts": [], + "seed": 42 + }, + "environment": { + "cliVersion": "v6.0.0", + "modelArchHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "deterministic": true, + "gpu": null, + "platform": "linux-x86_64", + "nodeVersion": "v22.22.1" + }, + "result": { + "metric": "val_loss", + "value": 0, + "unit": "nats", + "delta": 0, + "isNewBest": true, + "additional": { + "trainLoss": 0, + "durationSec": 0, + "lossCurve": [] + } + }, + "proof": { + "level": "signed" + }, + "timestamp": "2026-05-07T00:00:00.000Z", + "receiptId": "rcpt_N5AFVADNVGUIRK47RQVZRGKXV25EVQZ22W4SJJYRTIOVNRGGS3KA", + "signature": { + "alg": "ed25519", + "publicKey": "rUvlo2ndISYGTT5yrvv3GX2o5dmRLABCP05fojkhMtI=", + "value": "EqNZDLiRxXsFbR1vkw7dupyfaMHvQAQPyjU9goLeNNbN35uNIqCnUsEjD1uuLk2xOPfDP3unBClR/iLi3oquDQ==" + } +} diff --git a/schemas/research-receipt-v1.schema.json b/schemas/research-receipt-v1.schema.json new file mode 100644 index 0000000000000..74d82747c2dff --- /dev/null +++ b/schemas/research-receipt-v1.schema.json @@ -0,0 +1,217 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyper.space/schemas/research-receipt-v1.json", + "title": "Research Receipt v1", + "description": "Verifiable research receipt per RFC-001. A signed, schema-conformant record that an experiment was performed and produced a measurable result.", + "type": "object", + "required": [ + "version", + "receiptId", + "project", + "runNumber", + "peerId", + "inputs", + "environment", + "result", + "proof", + "signature", + "timestamp" + ], + "additionalProperties": false, + "properties": { + "$schema": { "type": "string", "format": "uri" }, + "version": { "const": 1 }, + + "receiptId": { + "type": "string", + "pattern": "^rcpt_[A-Z2-7]{52}$", + "description": "rcpt_ + base32(sha256(canonical_body)). Self-binding identifier." + }, + + "project": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", + "description": "Project slug under projects/ — must match an existing project directory." + }, + + "runNumber": { + "type": "integer", + "minimum": 0 + }, + + "peerId": { + "type": "string", + "pattern": "^12D3Koo[A-Za-z0-9]{45,55}$", + "description": "libp2p peer ID of the running agent." + }, + + "inputs": { + "type": "object", + "required": ["configHash", "configRef", "datasetHash", "seed"], + "additionalProperties": false, + "properties": { + "configHash": { "$ref": "#/$defs/sha256" }, + "configRef": { + "type": "string", + "description": "Repo-relative path to the config that was used." + }, + "datasetHash": { "$ref": "#/$defs/sha256" }, + "parentReceipts": { + "type": "array", + "items": { "$ref": "#/$defs/receiptIdRef" }, + "description": "Lineage. Empty for root/baseline receipts." + }, + "seed": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + } + } + }, + + "environment": { + "type": "object", + "required": ["cliVersion", "modelArchHash", "deterministic"], + "additionalProperties": true, + "properties": { + "cliVersion": { + "type": "string", + "pattern": "^v\\d+\\.\\d+\\.\\d+$" + }, + "modelArchHash": { "$ref": "#/$defs/sha256" }, + "deterministic": { "type": "boolean" }, + "gpu": { "type": ["string", "null"] }, + "platform": { "type": "string" }, + "nodeVersion": { "type": "string" } + } + }, + + "result": { + "type": "object", + "required": ["metric", "value", "delta", "isNewBest"], + "additionalProperties": false, + "properties": { + "metric": { + "type": "string", + "description": "Metric name (val_loss, sharpe, ndcg, dag_quality_score, ...)." + }, + "value": { "type": "number" }, + "unit": { "type": "string" }, + "delta": { + "type": "number", + "description": "Improvement over the previous network best on this project. Sign convention: negative for loss-style metrics, positive for score-style." + }, + "isNewBest": { "type": "boolean" }, + "additional": { + "type": "object", + "description": "Project-specific extra fields (lossCurve, durationSec, etc.)" + } + } + }, + + "proof": { + "type": "object", + "required": ["level"], + "oneOf": [ + { "$ref": "#/$defs/proofSigned" }, + { "$ref": "#/$defs/proofReplay" }, + { "$ref": "#/$defs/proofZk" }, + { "$ref": "#/$defs/proofTee" } + ] + }, + + "signature": { + "type": "object", + "required": ["alg", "publicKey", "value"], + "additionalProperties": false, + "properties": { + "alg": { "const": "ed25519" }, + "publicKey": { + "type": "string", + "description": "base64-encoded raw 32-byte public key." + }, + "value": { + "type": "string", + "description": "base64-encoded 64-byte Ed25519 signature over the canonical body (excluding the signature field itself)." + } + } + }, + + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "receiptIdRef": { + "type": "string", + "pattern": "^rcpt_[A-Z2-7]{52}$" + }, + + "proofSigned": { + "type": "object", + "required": ["level"], + "additionalProperties": false, + "properties": { + "level": { "const": "signed" } + } + }, + + "proofReplay": { + "type": "object", + "required": ["level", "replayInstructions"], + "additionalProperties": false, + "properties": { + "level": { "const": "replay" }, + "replayInstructions": { + "type": "object", + "required": ["command", "expectedHash"], + "properties": { + "command": { "type": "string" }, + "expectedHash": { "$ref": "#/$defs/sha256" } + } + } + } + }, + + "proofZk": { + "type": "object", + "required": ["level", "circuit", "proofData", "publicInputs"], + "additionalProperties": false, + "properties": { + "level": { + "type": "string", + "enum": ["zk-jolt", "zk-groth16", "zk-sp1"] + }, + "circuit": { + "type": "string", + "description": "Circuit identifier — e.g. 'training-step-v1'." + }, + "proofData": { + "type": "string", + "description": "base64-encoded proof bytes." + }, + "publicInputs": { "type": "object" } + } + }, + + "proofTee": { + "type": "object", + "required": ["level", "attestationChain"], + "additionalProperties": false, + "properties": { + "level": { "const": "tee-attestation" }, + "attestationChain": { + "type": "array", + "items": { "type": "string" }, + "description": "Ordered chain of TEE attestation reports (base64)." + } + } + } + } +} diff --git a/scripts/sign-receipt.js b/scripts/sign-receipt.js new file mode 100644 index 0000000000000..64aee09877e7c --- /dev/null +++ b/scripts/sign-receipt.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node +/* + * Reference signer for Research Receipts (RFC-001). + * + * Reads an unsigned receipt body, generates an Ed25519 keypair (or uses + * a provided one), produces the canonical signature, computes the + * receiptId, and writes the complete signed receipt. + * + * Usage: + * node scripts/sign-receipt.js <unsigned-body.json> [output.json] + * + * The unsigned body must include all fields except `receiptId`, + * `signature.value`, and (optionally) `signature.publicKey`. If no + * publicKey is present, a fresh keypair is generated and the private + * key is printed to stderr. + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { canonicalize, base32Encode } = require('./validate-receipt'); + +function signReceipt(body, privateKey, publicKeyB64) { + // Build the body to be hashed/signed: receipt without receiptId or + // signature.value. The signature publicKey/alg ARE included. + const bodyForHash = { ...body }; + delete bodyForHash.receiptId; + bodyForHash.signature = { + alg: 'ed25519', + publicKey: publicKeyB64, + }; + + const canonical = canonicalize(bodyForHash); + const sigBytes = crypto.sign(null, Buffer.from(canonical), privateKey); + const digest = crypto.createHash('sha256').update(canonical).digest(); + const receiptId = 'rcpt_' + base32Encode(digest).slice(0, 52); + + return { + ...body, + receiptId, + signature: { + alg: 'ed25519', + publicKey: publicKeyB64, + value: sigBytes.toString('base64'), + }, + }; +} + +function exportEd25519PublicKeyRaw(publicKey) { + // Strip the SPKI DER prefix (12 bytes) to get the raw 32-byte public key. + const der = publicKey.export({ format: 'der', type: 'spki' }); + return der.subarray(der.length - 32).toString('base64'); +} + +function main(argv) { + const args = argv.slice(2); + if (args.length < 1) { + console.error('Usage: sign-receipt.js <unsigned-body.json> [output.json]'); + process.exit(2); + } + const inputPath = args[0]; + const outputPath = args[1] || inputPath.replace(/\.json$/, '.signed.json'); + + const body = JSON.parse(fs.readFileSync(inputPath, 'utf8')); + + let privateKey, publicKeyB64; + if (body.signature?.publicKey && process.env.HYPERSPACE_PRIVATE_KEY) { + publicKeyB64 = body.signature.publicKey; + privateKey = crypto.createPrivateKey({ + key: Buffer.from(process.env.HYPERSPACE_PRIVATE_KEY, 'base64'), + format: 'der', + type: 'pkcs8', + }); + } else { + const kp = crypto.generateKeyPairSync('ed25519'); + privateKey = kp.privateKey; + publicKeyB64 = exportEd25519PublicKeyRaw(kp.publicKey); + const privDer = privateKey.export({ format: 'der', type: 'pkcs8' }); + console.error('# Generated fresh Ed25519 keypair'); + console.error('# HYPERSPACE_PRIVATE_KEY (base64, save securely):'); + console.error(privDer.toString('base64')); + console.error('# Public key (base64):'); + console.error(publicKeyB64); + } + + const signed = signReceipt(body, privateKey, publicKeyB64); + fs.writeFileSync(outputPath, JSON.stringify(signed, null, 2) + '\n'); + console.error(`# Wrote signed receipt to ${outputPath}`); + console.error(`# receiptId: ${signed.receiptId}`); +} + +if (require.main === module) { + main(process.argv); +} + +module.exports = { signReceipt }; diff --git a/scripts/validate-receipt.js b/scripts/validate-receipt.js new file mode 100644 index 0000000000000..244cdcf1ecbdc --- /dev/null +++ b/scripts/validate-receipt.js @@ -0,0 +1,239 @@ +#!/usr/bin/env node +/* + * Reference validator for Research Receipts (RFC-001). + * + * Performs the validation algorithm steps that don't require external + * dependencies (proof systems, network lookups): + * 1. Parse receipt JSON; reject if not schema-conformant. + * 2. Canonicalize body (excluding signature) via JCS (RFC 8785). + * 3. Verify Ed25519 signature against signature.publicKey. + * 4. Recompute receiptId; reject if mismatched. + * 7. Run proof verification for declared level (signed: nothing further). + * + * Steps 5, 6, 8, 9 (config/dataset hash resolution, project policy lookup, + * proof-level enforcement, transitive parent verification) are out of + * scope for this reference impl and tracked as follow-up work. + * + * Usage: + * node scripts/validate-receipt.js <path-to-receipt.json> + * node scripts/validate-receipt.js --all # validate all receipts in projects/ + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +// ---------------------------------------------------------------- // +// JCS canonicalization (RFC 8785) — minimal implementation. +// For a production system, use the `canonicalize` npm package. +// ---------------------------------------------------------------- // +function canonicalize(value) { + if (value === null || typeof value !== 'object') { + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return '[' + value.map(canonicalize).join(',') + ']'; + } + const keys = Object.keys(value).sort(); + return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}'; +} + +// ---------------------------------------------------------------- // +// Schema validation — we hand-roll a small subset to avoid pulling in ajv +// for a reference impl. Real validators should use ajv + the published +// JSON Schema in schemas/research-receipt-v1.schema.json. +// ---------------------------------------------------------------- // +function validateSchema(r) { + const errors = []; + const required = [ + 'version', 'receiptId', 'project', 'runNumber', 'peerId', + 'inputs', 'environment', 'result', 'proof', 'signature', 'timestamp' + ]; + for (const k of required) { + if (!(k in r)) errors.push(`missing required field: ${k}`); + } + if (r.version !== 1) errors.push(`unsupported version: ${r.version}`); + if (!/^rcpt_[A-Z2-7]{52}$/.test(r.receiptId || '')) { + errors.push(`invalid receiptId format: ${r.receiptId}`); + } + if (!/^[a-z0-9][a-z0-9-]{1,63}$/.test(r.project || '')) { + errors.push(`invalid project slug: ${r.project}`); + } + if (r.inputs) { + for (const k of ['configHash', 'configRef', 'datasetHash', 'seed']) { + if (!(k in r.inputs)) errors.push(`missing inputs.${k}`); + } + if (r.inputs.configHash && !/^sha256:[a-f0-9]{64}$/.test(r.inputs.configHash)) { + errors.push(`invalid inputs.configHash format`); + } + } + if (r.proof && !['signed', 'replay', 'zk-jolt', 'zk-groth16', 'zk-sp1', 'tee-attestation'].includes(r.proof.level)) { + errors.push(`unknown proof level: ${r.proof.level}`); + } + if (r.signature && r.signature.alg !== 'ed25519') { + errors.push(`unsupported signature alg: ${r.signature.alg}`); + } + return errors; +} + +// ---------------------------------------------------------------- // +// Base32 (RFC 4648, no padding, uppercase A-Z2-7) — used for receiptId. +// ---------------------------------------------------------------- // +function base32Encode(buf) { + const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + let bits = 0, value = 0, out = ''; + for (const byte of buf) { + value = (value << 8) | byte; + bits += 8; + while (bits >= 5) { + out += ALPHABET[(value >>> (bits - 5)) & 0x1f]; + bits -= 5; + } + } + if (bits > 0) { + out += ALPHABET[(value << (5 - bits)) & 0x1f]; + } + return out; +} + +// ---------------------------------------------------------------- // +// Receipt validation. +// ---------------------------------------------------------------- // +function validateReceipt(receipt) { + const result = { valid: false, receiptId: receipt.receiptId, checks: {}, errors: [] }; + + // Step 1: schema + const schemaErrors = validateSchema(receipt); + result.checks.schema = schemaErrors.length === 0; + if (!result.checks.schema) { + result.errors.push(...schemaErrors); + return result; + } + + // Step 2 + 4: canonicalize body without the signature.value, recompute receiptId. + // The receiptId itself is also excluded from the body it commits to (otherwise + // we'd have a chicken-and-egg problem: id depends on body which contains id). + const { signature, receiptId, ...body } = receipt; + // We DO include the publicKey + alg in the body so the signer commits to which + // key signed. We do NOT include signature.value. + const bodyForHash = { ...body, signature: { alg: signature.alg, publicKey: signature.publicKey } }; + const canonical = canonicalize(bodyForHash); + const digest = crypto.createHash('sha256').update(canonical).digest(); + const computedId = 'rcpt_' + base32Encode(digest).slice(0, 52); + result.checks.receiptId = computedId === receiptId; + if (!result.checks.receiptId) { + result.errors.push(`receiptId mismatch: declared=${receiptId} computed=${computedId}`); + } + + // Step 3: signature verification. + try { + const pubKey = crypto.createPublicKey({ + key: Buffer.concat([ + // Ed25519 SPKI prefix (DER): 12 bytes for OID + algorithm identifier. + Buffer.from('302a300506032b6570032100', 'hex'), + Buffer.from(signature.publicKey, 'base64'), + ]), + format: 'der', + type: 'spki', + }); + const sigBytes = Buffer.from(signature.value, 'base64'); + result.checks.signature = crypto.verify(null, Buffer.from(canonical), pubKey, sigBytes); + if (!result.checks.signature) { + result.errors.push('Ed25519 signature does not verify against the canonical body'); + } + } catch (err) { + result.checks.signature = false; + result.errors.push(`signature verification error: ${err.message}`); + } + + // Step 7: proof-level-specific verification. + switch (receipt.proof.level) { + case 'signed': + // Already covered by step 3. + result.checks.proof = result.checks.signature; + break; + case 'replay': + // Reference impl does not run replays — would require sandboxed exec. + result.checks.proof = 'skipped (replay verification not implemented in reference validator)'; + break; + case 'zk-jolt': + case 'zk-groth16': + case 'zk-sp1': + // Reference impl does not verify zk proofs — would require linking the + // appropriate proof-system library. Production validators must wire + // these in. + result.checks.proof = 'skipped (zk verification requires external proof-system library)'; + break; + case 'tee-attestation': + result.checks.proof = 'skipped (TEE attestation verification not implemented in reference validator)'; + break; + } + + result.valid = result.checks.schema + && result.checks.receiptId + && result.checks.signature; + return result; +} + +// ---------------------------------------------------------------- // +// CLI entrypoint. +// ---------------------------------------------------------------- // +function findAllReceipts(rootDir) { + const receipts = []; + const projectsDir = path.join(rootDir, 'projects'); + if (!fs.existsSync(projectsDir)) return receipts; + for (const project of fs.readdirSync(projectsDir, { withFileTypes: true })) { + if (!project.isDirectory()) continue; + const receiptsDir = path.join(projectsDir, project.name, 'receipts'); + if (!fs.existsSync(receiptsDir)) continue; + for (const file of fs.readdirSync(receiptsDir)) { + if (file.endsWith('.json')) { + receipts.push(path.join(receiptsDir, file)); + } + } + } + return receipts; +} + +function main(argv) { + const args = argv.slice(2); + if (args.length === 0 || args[0] === '-h' || args[0] === '--help') { + console.error('Usage: validate-receipt.js <path>... | --all'); + process.exit(2); + } + + let paths; + if (args[0] === '--all') { + paths = findAllReceipts(process.cwd()); + if (paths.length === 0) { + console.log('No receipts found under projects/*/receipts/'); + process.exit(0); + } + } else { + paths = args; + } + + let allValid = true; + for (const p of paths) { + const json = JSON.parse(fs.readFileSync(p, 'utf8')); + const result = validateReceipt(json); + const tag = result.valid ? 'VALID ' : 'INVALID'; + console.log(`[${tag}] ${p}`); + for (const [k, v] of Object.entries(result.checks)) { + console.log(` ${k}: ${v}`); + } + for (const err of result.errors) { + console.log(` ! ${err}`); + } + if (!result.valid) allValid = false; + } + process.exit(allValid ? 0 : 1); +} + +if (require.main === module) { + main(process.argv); +} + +module.exports = { validateReceipt, canonicalize, base32Encode }; From 04d3aea84fc250db6023531cd2be34a4704e7ec3 Mon Sep 17 00:00:00 2001 From: Micka Delcato <advislumiazap@gmail.com> Date: Thu, 7 May 2026 11:25:12 +0200 Subject: [PATCH 2/2] fix: VISION.md broken RFC links + add the test file claimed in PR description Two issues found in self-review of PR #22: 1. VISION.md linked to docs/rfcs/RFC-004-modular-agents.md and RFC-005-reality-anchoring.md which do not exist (those RFCs are stubs to be written later). The PR was advertising broken links. Fix: drop the markdown links; keep the section content describing each transformation. 2. The PR description referenced scripts/validate-receipt.test.js as part of the reference implementation, but the file was never actually committed (lost during a branch cleanup). Add it now. 17 tests covering canonicalisation, base32 encoding, schema validation, signature verification, receiptId derivation, and sign+validate round-trip. All pass on Node 22 / 24. Run with: node --test scripts/validate-receipt.test.js --- docs/VISION.md | 4 +- scripts/validate-receipt.test.js | 208 +++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 scripts/validate-receipt.test.js diff --git a/docs/VISION.md b/docs/VISION.md index 07950eddebbff..452a297b6c84e 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -92,7 +92,7 @@ This depends on Transformation 1 (verifiable receipts) and is the strongest argu > skills, each independently versioned, signed, and hot-swappable. Skills > migrate between agents at runtime. -**RFC:** [`docs/rfcs/RFC-004-modular-agents.md`](rfcs/RFC-004-modular-agents.md) *(to be written)* +**RFC:** RFC-004 (to be written) **The change:** A skill is a sandboxed WebAssembly module with a declared capability manifest (network, filesystem, model access, GPU). Agents are configurations that wire skills into a graph. Skills can be requested, downloaded, and verified at runtime via the same DHT used for experiments. @@ -109,7 +109,7 @@ This depends on Transformation 1 (verifiable receipts) and is the strongest argu > connections to real-world feedback loops — outputs that fail in production > get downweighted regardless of synthetic-benchmark scores. -**RFC:** [`docs/rfcs/RFC-005-reality-anchoring.md`](rfcs/RFC-005-reality-anchoring.md) *(to be written)* +**RFC:** RFC-005 (to be written) **The change:** Add a `RealityProbe` adapter: lightweight scorers that measure outputs against real-world signals (GitHub PR merge rate, Stack Overflow answer acceptance, Wikipedia edit retention, user thumbs-up/down on inference). Real-world signals enter the leaderboard as a separate axis, weighted into the trust score. diff --git a/scripts/validate-receipt.test.js b/scripts/validate-receipt.test.js new file mode 100644 index 0000000000000..1a4c18547efa9 --- /dev/null +++ b/scripts/validate-receipt.test.js @@ -0,0 +1,208 @@ +'use strict'; + +/* + * Unit tests for scripts/validate-receipt.js using Node.js native test runner. + * Run with: node --test scripts/validate-receipt.test.js + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); + +const { validateReceipt, canonicalize, base32Encode } = require('./validate-receipt'); +const { signReceipt } = require('./sign-receipt'); + +// ---------------------------------------------------------------- // +// Helpers +// ---------------------------------------------------------------- // +function makeUnsignedBody(overrides = {}) { + return { + $schema: 'https://hyper.space/schemas/research-receipt-v1.json', + version: 1, + project: 'gpt2-tinystories', + runNumber: 1, + peerId: '12D3KooWTestTestTestTestTestTestTestTestTestTestTest', + inputs: { + configHash: 'sha256:' + 'a'.repeat(64), + configRef: 'projects/gpt2-tinystories/baseline/config.yaml', + datasetHash: 'sha256:' + 'b'.repeat(64), + parentReceipts: [], + seed: 42, + }, + environment: { + cliVersion: 'v6.0.0', + modelArchHash: 'sha256:' + 'c'.repeat(64), + deterministic: true, + gpu: 'H100', + platform: 'linux-x86_64', + nodeVersion: 'v22.22.1', + }, + result: { + metric: 'val_loss', + value: 1.23, + unit: 'nats', + delta: -0.05, + isNewBest: true, + additional: { trainLoss: 1.10, durationSec: 287, lossCurve: [1.5, 1.4, 1.23] }, + }, + proof: { level: 'signed' }, + timestamp: '2026-05-07T12:34:56.789Z', + ...overrides, + }; +} + +function signWithFreshKey(body) { + const kp = crypto.generateKeyPairSync('ed25519'); + const der = kp.publicKey.export({ format: 'der', type: 'spki' }); + const pubB64 = der.subarray(der.length - 32).toString('base64'); + return signReceipt(body, kp.privateKey, pubB64); +} + +// ---------------------------------------------------------------- // +// canonicalize +// ---------------------------------------------------------------- // +test('canonicalize sorts object keys alphabetically', () => { + const a = canonicalize({ b: 1, a: 2, c: 3 }); + const b = canonicalize({ a: 2, b: 1, c: 3 }); + assert.equal(a, b); + assert.equal(a, '{"a":2,"b":1,"c":3}'); +}); + +test('canonicalize handles nested objects and arrays', () => { + const c = canonicalize({ z: [1, { y: 2, x: 3 }], a: null }); + assert.equal(c, '{"a":null,"z":[1,{"x":3,"y":2}]}'); +}); + +test('canonicalize is deterministic across equivalent inputs', () => { + const obj1 = { foo: 'bar', nested: { x: 1, y: 2 } }; + const obj2 = { nested: { y: 2, x: 1 }, foo: 'bar' }; + assert.equal(canonicalize(obj1), canonicalize(obj2)); +}); + +// ---------------------------------------------------------------- // +// base32Encode +// ---------------------------------------------------------------- // +test('base32Encode produces RFC4648 alphabet output', () => { + const out = base32Encode(Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff])); + assert.match(out, /^[A-Z2-7]+$/); + assert.equal(out, '77777777'); +}); + +test('base32Encode of zero buffer is all A', () => { + const out = base32Encode(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00])); + assert.equal(out, 'AAAAAAAA'); +}); + +// ---------------------------------------------------------------- // +// validateReceipt — happy path +// ---------------------------------------------------------------- // +test('validateReceipt accepts a properly signed receipt', () => { + const signed = signWithFreshKey(makeUnsignedBody()); + const result = validateReceipt(signed); + assert.equal(result.valid, true, JSON.stringify(result, null, 2)); + assert.equal(result.checks.schema, true); + assert.equal(result.checks.receiptId, true); + assert.equal(result.checks.signature, true); +}); + +test('validateReceipt rejects a tampered result.value', () => { + const signed = signWithFreshKey(makeUnsignedBody()); + signed.result.value = 999.99; + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.ok(!result.checks.receiptId || !result.checks.signature); +}); + +test('validateReceipt rejects an invalid signature', () => { + const signed = signWithFreshKey(makeUnsignedBody()); + signed.signature.value = Buffer.alloc(64, 0).toString('base64'); + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.equal(result.checks.signature, false); +}); + +test('validateReceipt rejects a mismatched receiptId', () => { + const signed = signWithFreshKey(makeUnsignedBody()); + const last3 = signed.receiptId.slice(-3); + const replacement = last3 === 'AAA' ? 'BBB' : 'AAA'; + signed.receiptId = signed.receiptId.slice(0, -3) + replacement; + const result = validateReceipt(signed); + assert.equal(result.valid, false); +}); + +// ---------------------------------------------------------------- // +// validateReceipt — schema errors +// ---------------------------------------------------------------- // +test('validateReceipt rejects missing required fields', () => { + const signed = signWithFreshKey(makeUnsignedBody()); + delete signed.peerId; + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.equal(result.checks.schema, false); + assert.ok(result.errors.some(e => e.includes('peerId'))); +}); + +test('validateReceipt rejects unsupported version', () => { + const signed = signWithFreshKey(makeUnsignedBody({ version: 2 })); + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.equal(result.checks.schema, false); +}); + +test('validateReceipt rejects malformed configHash', () => { + const body = makeUnsignedBody(); + body.inputs.configHash = 'md5:notarealhash'; + const signed = signWithFreshKey(body); + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.equal(result.checks.schema, false); +}); + +test('validateReceipt rejects unknown proof level', () => { + const body = makeUnsignedBody(); + body.proof = { level: 'magical-trust-me-bro' }; + const signed = signWithFreshKey(body); + const result = validateReceipt(signed); + assert.equal(result.valid, false); + assert.equal(result.checks.schema, false); +}); + +test('validateReceipt rejects unsupported signature algorithm', () => { + const body = makeUnsignedBody(); + const signed = signWithFreshKey(body); + signed.signature.alg = 'rsa-pss'; + const result = validateReceipt(signed); + assert.equal(result.valid, false); +}); + +// ---------------------------------------------------------------- // +// signReceipt round-trip +// ---------------------------------------------------------------- // +test('signReceipt + validateReceipt round-trip preserves all fields', () => { + const body = makeUnsignedBody(); + const signed = signWithFreshKey(body); + for (const k of Object.keys(body)) { + assert.deepEqual(signed[k], body[k], `field ${k} mutated`); + } + assert.match(signed.receiptId, /^rcpt_[A-Z2-7]{52}$/); + assert.ok(signed.signature.value); +}); + +test('signReceipt produces deterministic receiptId for same body + same key (Ed25519 RFC 8032)', () => { + const body = makeUnsignedBody(); + const kp = crypto.generateKeyPairSync('ed25519'); + const der = kp.publicKey.export({ format: 'der', type: 'spki' }); + const pubB64 = der.subarray(der.length - 32).toString('base64'); + + const a = signReceipt(JSON.parse(JSON.stringify(body)), kp.privateKey, pubB64); + const b = signReceipt(JSON.parse(JSON.stringify(body)), kp.privateKey, pubB64); + assert.equal(a.receiptId, b.receiptId); + assert.equal(a.signature.value, b.signature.value); +}); + +test('signReceipt produces different receiptId for different keys', () => { + const body = makeUnsignedBody(); + const a = signWithFreshKey(JSON.parse(JSON.stringify(body))); + const b = signWithFreshKey(JSON.parse(JSON.stringify(body))); + assert.notEqual(a.receiptId, b.receiptId); +});