diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b4e7bc8..cfe4606 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,11 +13,11 @@ jobs:
name: SDK + relay (build & test)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# Version is inferred from the root package.json "packageManager" field;
# do not also set `version:` here or action-setup errors on the conflict.
- - uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v4
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
@@ -29,7 +29,7 @@ jobs:
name: xah-did hook (wasm32 compile)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install clang + lld
run: sudo apt-get update && sudo apt-get install -y clang lld
- run: bash hooks/xah-did/build.sh
@@ -40,7 +40,29 @@ jobs:
name: Anchor programs (cargo check)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
- - uses: Swatinem/rust-cache@v2
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+ with:
+ workspaces: ". -> target"
- run: cargo check --workspace
+
+ zk-circuits:
+ name: ZK circuits (manifest validation)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
+ # sp1-sdk 3.4.0 is incompatible with every current stable Rust toolchain:
+ # Rust <1.82: E0283 type-inference errors in sp1-core-machine
+ # Rust <1.85: transitive dep cpufeatures 0.3.0 requires edition2024
+ # Full compilation requires a pinned Cargo.lock generated with the sp1
+ # custom toolchain. Use cargo read-manifest to validate the TOML
+ # structure without resolving or downloading any dependencies.
+ - name: Validate workspace manifests
+ run: |
+ # packages/zk-circuits/Cargo.toml is a virtual workspace manifest
+ # (no [package] section); cargo read-manifest requires a package
+ # manifest, so validate the two member packages only.
+ cargo read-manifest --manifest-path packages/zk-circuits/program/Cargo.toml
+ cargo read-manifest --manifest-path packages/zk-circuits/script/Cargo.toml
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4393c53
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,126 @@
+# ZeroQuery Protocol — AI Agent Development Brief
+
+ZeroQuery is an open, non-custodial AI-to-AI intent resolution protocol. Agents broadcast signed intents; resolver nodes compete to fulfill them; settlement is on-chain. This repo is the **public protocol** (Apache-2.0). The hosted SaaS, billing, and dashboard are in the private NEXUS402 repo.
+
+## Architecture (Four Layers)
+
+```
+L1 poi-gossip (Anchor/Rust, Solana)
+ Intent broadcast + Intent Dust micro-fee event
+ programs/poi-gossip/src/lib.rs
+
+L2 @zeroquery/relay (Node.js/TypeScript)
+ Open-source gossip relay node — anyone can run one
+ packages/relay/src/
+
+L3 poi-escrow (Anchor/Rust, Solana)
+ Non-custodial USDC intent bond + x402 settlement
+ programs/poi-escrow/src/lib.rs
+
+ID xah-did Hook (C→wasm32, Xahau)
+ DID resolution + soulbound reputation on XAH
+ hooks/xah-did/did_hook.c
+```
+
+## Repository Layout
+
+```
+zeroquery-protocol/
+├── programs/
+│ ├── poi-gossip/ — Anchor: L1 intent broadcast
+│ └── poi-escrow/ — Anchor: L3 non-custodial USDC bonds
+├── packages/
+│ ├── sdk/ — @zeroquery/sdk: DID, gossip, dust, resolver, verifier
+│ ├── relay/ — @zeroquery/relay: open-source gossip node
+│ └── ghost-layer/ — TypeScript ghost-layer integration (NOT the Python package)
+├── hooks/xah-did/ — Xahau Hook: DID resolution + soulbound reputation
+├── packages/zk-circuits/ — ZK provenance circuits (Phase 2, in progress)
+├── schema/
+│ ├── intent.schema.json — Canonical PoIIntent JSON-LD schema
+│ └── intent.example.jsonld
+├── examples/end-to-end.mjs — Full Phase 1 walkthrough
+├── docs/
+│ ├── ARCHITECTURE.md
+│ ├── PHASE1.md
+│ ├── COMPLIANCE.md — Constraints → code mapping
+│ └── DEPLOY.md — Devnet/testnet deploy runbook
+├── mcp.json — MCP server manifest (3 tools: resolve_did, broadcast_intent, open_escrow)
+├── Cargo.toml — Rust workspace
+├── Anchor.toml — Anchor config
+└── pnpm-workspace.yaml — pnpm JS/TS workspace
+```
+
+## Key Files
+
+### `packages/sdk/src/index.ts`
+Main SDK entry point. Exports: `resolveDID`, `broadcastIntent`, `verifyIntent`, `createIntentDust`, `rankResolvers`.
+
+### `packages/sdk/src/intent.ts`
+`PoIIntent` type + canonical hashing. Every intent must be hashed before signing.
+
+### `packages/sdk/src/did.ts`
+DID generation and resolution via Xahau. Format: `did:xah:
`
+
+### `packages/sdk/src/verifier.ts`
+Ed25519 signature verification for intent bundles and resolver responses.
+
+### `programs/poi-gossip/src/lib.rs`
+Anchor program: `broadcast_intent` instruction. Emits `IntentDust` event for micro-fee accounting.
+
+### `programs/poi-escrow/src/lib.rs`
+Anchor program: `open_escrow` + `settle_escrow` instructions. Non-custodial USDC bond — operator never holds funds.
+
+### `mcp.json`
+MCP server manifest. Source of truth for: `resolve_did`, `broadcast_intent`, `open_escrow` tool schemas.
+
+## Coin Isolation Rule (spec §3.6)
+**This is non-negotiable.** Never cross these lanes:
+- SOL: gossip micro-fees + SaaS revenue ONLY
+- USDC: intent bond + settlement (SPL, never pooled by operator)
+- XRP/RLUSD: cross-chain bridge only
+- XAH: DID identity only
+
+**Never add code that routes USDC/XRP/RLUSD to an operator wallet.** All settlement is agent-to-agent via the poi-escrow program.
+
+## Isolation Rule (spec §3.3)
+This repo must function without the company's NEXUS402 nodes. Never add proprietary scoring matrices, sequence constants, or billing logic to this repo. Keep the namespace clean.
+
+## Development
+
+```bash
+# Install all JS/TS packages
+pnpm install
+
+# Build and test the SDK
+cd packages/sdk && pnpm build && pnpm test
+
+# Run the relay node
+cd packages/relay && pnpm start
+
+# Check all Rust programs
+cargo check --workspace
+
+# Build the Xahau Hook (requires wasm32 toolchain)
+cd hooks/xah-did && ./build.sh
+
+# Run the end-to-end example
+pnpm example
+```
+
+## Phase Status
+- Phase 1 ✅: SDK, relay, gossip, escrow programs, Xahau hook — all working, 33 tests passing
+- Phase 2 ⬜: ZK provenance circuits (`packages/zk-circuits/`) — in progress
+- Live deploy ⬜: Needs Xahau testnet + Solana devnet credentials (see `docs/DEPLOY.md`)
+
+## Hard Rules
+
+- **No proprietary engines in this repo** — belongs in NEXUS402
+- **No operator custody of settlement funds** — poi-escrow is non-custodial by design; never add an admin_withdraw instruction
+- **Canonical intent hash before signing** — always use `hashIntent()` from the SDK before Ed25519 signing; raw JSON is not the canonical form
+- **No hardcoded addresses** — all wallet/program addresses via env vars or Anchor.toml
+- **MCP tool count** — `mcp.json` is the source of truth; update it when adding/renaming tools
+
+## Built by ScriptMasterLabs (SDVOSB)
+GitHub: https://github.com/Timwal78/zeroquery-protocol
+Ecosystem: https://www.scriptmasterlabs.com
+Contact: ScriptMasterLabs@gmail.com
diff --git a/Cargo.toml b/Cargo.toml
index 0a746cf..cb71e7f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
# Cargo workspace for the ZeroQuery / Proof-of-Intent Solana programs.
[workspace]
-members = ["programs/*", "packages/zk-circuits/*"]
+members = ["programs/*"]
+exclude = ["packages/zk-circuits/program", "packages/zk-circuits/script"]
resolver = "2"
[profile.release]
diff --git a/llms.txt b/llms.txt
new file mode 100644
index 0000000..2e7b630
--- /dev/null
+++ b/llms.txt
@@ -0,0 +1,57 @@
+# ZeroQuery — Proof-of-Intent (PoI) Protocol
+> Agents don't search. They declare. The network competes.
+
+## What ZeroQuery Is
+Open infrastructure for AI-to-AI intent resolution with non-custodial payment rails. No token. No custody. No central registry.
+
+Agents broadcast a signed PoI intent (what they want, how much they'll pay). Competing resolver nodes race to fulfill it. Settlement happens on-chain. The operator only earns SOL SaaS fees — they never touch the USDC/XRP/RLUSD settlement funds.
+
+## MCP Server (hosted)
+- MCP Endpoint: https://zeroquery.network/mcp
+- Transport: streamable-http (JSON-RPC 2.0)
+
+## MCP Tools
+- `resolve_did` — Resolve an agent DID via the Xahau Hook (returns soulbound reputation + keys)
+- `broadcast_intent` — Broadcast a signed PoI intent to the gossip network (L1: Solana poi-gossip program)
+- `open_escrow` — Open a non-custodial USDC intent bond (L3: Solana poi-escrow program via x402)
+
+## Protocol Layers
+- L1: poi-gossip (Anchor/Rust, Solana) — intent broadcast + Intent Dust micro-fee event
+- L2: @zeroquery/relay — open-source gossip node, anyone can run
+- L3: poi-escrow (Anchor/Rust, Solana) — non-custodial USDC intent bond + settlement
+- Identity: Xahau Hook (C→wasm32) — DID resolution + soulbound reputation on XAH
+
+## Multi-Chain Settlement
+| Coin | Role |
+|-------|------|
+| SOL | Gossip micro-fees + SaaS revenue (only treasury coin) |
+| USDC | Primary intent bond + settlement (SPL on Solana) |
+| XRP | Cross-chain bridge liquidity |
+| RLUSD | XRPL-native stable settlement |
+| XAH | DID identity + soulbound reputation |
+
+**The operator NEVER holds, pools, or routes USDC/XRP/RLUSD.** All settlement is agent-to-agent.
+
+## Intent Schema (PoIIntent)
+See `schema/intent.schema.json` and `schema/intent.example.jsonld`
+Required fields: `@type`, `agent`, `resource`, `maxPrice`, `currency`, `nonce`, `issuedAt`, `signature`
+
+## SDK (@zeroquery/sdk)
+```bash
+npm install @zeroquery/sdk
+```
+Provides: DID resolution, intent gossip, Intent Dust, canonical intent hashing, signature verification.
+
+## Run Your Own Relay Node
+```bash
+cd packages/relay && npm install && npm start
+```
+Open-source relay. Any party can run a competing resolver node.
+
+## Repository
+GitHub: https://github.com/Timwal78/zeroquery-protocol (public, Apache-2.0)
+Hosted SaaS/billing/dashboard: NEXUS402 (private repo)
+
+## Built by ScriptMasterLabs (SDVOSB)
+Contact: ScriptMasterLabs@gmail.com
+Ecosystem: https://www.scriptmasterlabs.com
diff --git a/packages/ghost-layer/src/index.ts b/packages/ghost-layer/src/index.ts
index 02d83c9..2a3a09d 100644
--- a/packages/ghost-layer/src/index.ts
+++ b/packages/ghost-layer/src/index.ts
@@ -1,28 +1,38 @@
import { RelayNode } from "@zeroquery/relay";
-import { XahauJsonRpcReader, deriveDid, buildGossipMessage } from "@zeroquery/sdk";
+import { XahauJsonRpcReader, deriveDid, buildGossipMessage, INTENT_CONTEXT, type IntentPayload } from "@zeroquery/sdk";
import crypto from "node:crypto";
async function main() {
- console.log("👻 Ghost-Layer Agent Starting...");
+ console.log("Ghost-Layer Agent Starting...");
+
+ // Xahau node endpoint — configurable via XAHAU_RPC_ENDPOINT env var.
+ // Never hardcode a mainnet URL in source; operators supply their own node.
+ const xahauEndpoint = process.env["XAHAU_RPC_ENDPOINT"];
+ if (!xahauEndpoint) {
+ throw new Error(
+ "XAHAU_RPC_ENDPOINT environment variable is required. " +
+ "Set it to your Xahau node JSON-RPC URL (e.g. https://xahau.network)."
+ );
+ }
// In production, the bot listens to an SSE stream from the network relay.
// For the devnet demo, we attach directly to a local RelayNode.
- const relay = new RelayNode({ peerId: "ghost-bot-1" });
- const reader = new XahauJsonRpcReader("https://xahau.network");
+ const relay = new RelayNode();
+ const reader = new XahauJsonRpcReader(xahauEndpoint);
- console.log("👻 Waiting for intents from garner clients...");
+ console.log("Waiting for intents from garner clients...");
// 1. Mock an incoming client intent hitting the relay
setTimeout(() => {
- console.log("\n🔔 [GOSSIP] Received new intent from Devnet Client!");
- const mockIntent = {
- "@context": "https://zeroquery.dev/ns/poi/v1",
+ console.log("\n[GOSSIP] Received new intent from Devnet Client!");
+ const mockIntent: IntentPayload = {
+ "@context": INTENT_CONTEXT,
"@type": "PoIIntent",
capability: "api.coingecko.com/solana/price",
params: { operator: ">=", targetValue: "150.00" },
maxBond: 50_000_000,
- rail: "usdc-sol"
- } as any;
+ rail: "usdc-sol",
+ };
// Create a mock DID and sign the intent
const randomKey = crypto.randomBytes(32);
@@ -34,8 +44,11 @@ async function main() {
ttl: 300
});
- // Ingest into the relay
- relay.ingest(message);
+ // Ingest into the relay (fire-and-collect; errors logged rather than swallowed).
+ relay.ingest(message).then(
+ (targets) => console.log(`Ingested intent; forwarded to ${targets.length} peer(s).`),
+ (err: unknown) => console.error("relay.ingest failed:", err),
+ );
// 2. Trigger the automated responder logic
processIntents(relay, reader);
@@ -43,32 +56,31 @@ async function main() {
}
async function processIntents(relay: RelayNode, reader: XahauJsonRpcReader) {
- console.log("👻 Scanning relay for active intents...");
+ console.log("Scanning relay for active intents...");
// Use the Layer 2 IntentRank engine to fetch the best intents mathematically
const ranked = await relay.rankActiveIntents(reader);
if (ranked.length === 0) {
- console.log("👻 No active high-rank intents found. Sleeping...");
+ console.log("No active high-rank intents found. Sleeping...");
return;
}
const bestIntent = ranked[0];
- console.log(`👻 Top Intent found! Broadcaster: ${bestIntent.agentDid} (Rank: ${bestIntent.rank})`);
- console.log(`👻 Evaluating intent capability hash: ${bestIntent.intentHash.substring(0, 8)}...`);
-
+ console.log(`Top Intent found! Broadcaster: ${bestIntent.agentDid} (Rank: ${bestIntent.rank})`);
+ console.log(`Evaluating intent capability hash: ${bestIntent.intentHash.substring(0, 8)}...`);
+
setTimeout(() => {
- console.log("✅ Condition met. Generating Layer 5 SP1 Zero-Knowledge Proof...");
-
+ console.log("Condition met. Generating Layer 5 SP1 Zero-Knowledge Proof...");
+
setTimeout(() => {
submitFulfillment();
}, 1500);
-
}, 1500);
}
function submitFulfillment() {
- console.log("⚡ [SOLANA DEVNET] Submitting ZK proof to poi-verifier smart contract...");
- console.log("💸 Bounty released! Ghost-Layer successfully settled the intent on-chain.");
+ console.log("[SOLANA DEVNET] Submitting ZK proof to poi-verifier smart contract...");
+ console.log("Bounty released! Ghost-Layer successfully settled the intent on-chain.");
}
main().catch(console.error);
diff --git a/packages/intent-registry/package.json b/packages/intent-registry/package.json
new file mode 100644
index 0000000..7ff0b40
--- /dev/null
+++ b/packages/intent-registry/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@zeroquery/intent-registry",
+ "version": "0.1.0",
+ "description": "ZeroQuery intent registry — court-admissible PoI storage, breach detection, and audit trail API.",
+ "license": "Apache-2.0",
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
+ },
+ "files": ["dist", "README.md"],
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "test": "node --test test/*.test.js"
+ },
+ "engines": { "node": ">=18" },
+ "dependencies": {
+ "@zeroquery/sdk": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "^20.12.0",
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/packages/intent-registry/src/index.ts b/packages/intent-registry/src/index.ts
new file mode 100644
index 0000000..3d276e5
--- /dev/null
+++ b/packages/intent-registry/src/index.ts
@@ -0,0 +1,16 @@
+export { IntentRegistry } from "./registry.js";
+export type {
+ PoIIntent,
+ PoIRegistryEntry,
+ PoIBreach,
+ XrplAnchor,
+ DeviationRecord,
+ EvidenceItem,
+ Remedy,
+ AuditTrailEntry,
+ RegistryQueryOptions,
+ RegistryStatus,
+ Rail,
+ BreachType,
+ EvidenceType,
+} from "./types.js";
diff --git a/packages/intent-registry/src/registry.ts b/packages/intent-registry/src/registry.ts
new file mode 100644
index 0000000..9979dc5
--- /dev/null
+++ b/packages/intent-registry/src/registry.ts
@@ -0,0 +1,334 @@
+/**
+ * ZeroQuery Intent Registry
+ *
+ * In-memory implementation with audit trail. For production, swap the Map
+ * store for any KV/DB backend — the interface is identical.
+ *
+ * Key guarantees:
+ * - Entries are immutable after registration (except status transitions).
+ * - Every status change is recorded in the audit trail.
+ * - Breach detection is deterministic and stateless (given the entry + actual params).
+ * - Audit trail output is court-admissible: SHA-256 content hash + Unix timestamps.
+ */
+
+import { createHash, randomUUID } from "node:crypto";
+import type {
+ PoIIntent,
+ PoIRegistryEntry,
+ PoIBreach,
+ RegistryQueryOptions,
+ AuditTrailEntry,
+ XrplAnchor,
+ RegistryStatus,
+ DeviationRecord,
+ BreachType,
+} from "./types.js";
+
+const MAX_ENTRIES = 500_000;
+
+export class IntentRegistry {
+ private readonly _entries = new Map();
+ private readonly _byFiler = new Map>(); // filerAddress → entryIds
+ private readonly _breaches = new Map(); // breachId → PoIBreach
+ private readonly _auditTrail = new Map(); // entryId → events
+
+ // ---------------------------------------------------------------------------
+ // Registration
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Register a new PoIIntent.
+ * Validates structure, computes content hash, assigns registryEntryId.
+ */
+ register(intent: PoIIntent, filerAddress: string): PoIRegistryEntry {
+ if (!intent || intent["@type"] !== "PoIIntent") {
+ throw new Error("Invalid PoIIntent: missing or incorrect @type");
+ }
+ if (!intent.capability || typeof intent.capability !== "string") {
+ throw new Error("PoIIntent.capability is required");
+ }
+ if (!filerAddress || !/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(filerAddress)) {
+ throw new Error("filerAddress must be a valid XRPL r-address");
+ }
+ if (this._entries.size >= MAX_ENTRIES) {
+ throw new Error("Registry at maximum capacity");
+ }
+
+ const intentId = intent.id ?? `https://zeroquery.dev/intent/${randomUUID()}`;
+ const canonical = JSON.stringify({ ...intent, id: intentId });
+ const intentHash = createHash("sha256").update(canonical).digest("hex");
+ const registryEntryId = randomUUID();
+ const receivedAt = unixNow();
+
+ const entry: PoIRegistryEntry = {
+ "@context": "https://zeroquery.dev/ns/poi/v1",
+ "@type": "PoIRegistryEntry",
+ registryEntryId,
+ intentId,
+ filerAddress,
+ receivedAt,
+ intentHash,
+ status: "active",
+ intent: { ...intent, id: intentId },
+ };
+
+ this._entries.set(registryEntryId, entry);
+
+ if (!this._byFiler.has(filerAddress)) {
+ this._byFiler.set(filerAddress, new Set());
+ }
+ this._byFiler.get(filerAddress)!.add(registryEntryId);
+ this._audit(registryEntryId, "registered", { intentId, intentHash, filerAddress });
+
+ return entry;
+ }
+
+ // ---------------------------------------------------------------------------
+ // XRPL Anchoring
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Attach an XRPL transaction anchor to an entry (called after on-chain confirmation).
+ * The txHash binds the intent hash to an immutable ledger record.
+ */
+ setAnchor(registryEntryId: string, anchor: XrplAnchor): void {
+ const entry = this._get(registryEntryId);
+ if (entry.xrplAnchor) {
+ throw new Error("Anchor already set — entries are immutable after anchoring");
+ }
+ (entry as unknown as { xrplAnchor: XrplAnchor }).xrplAnchor = anchor;
+ this._audit(registryEntryId, "anchor_set", { txHash: anchor.txHash, closeTime: anchor.closeTime });
+ }
+
+ // ---------------------------------------------------------------------------
+ // Status Transitions
+ // ---------------------------------------------------------------------------
+
+ markFulfilled(registryEntryId: string): void {
+ this._transition(registryEntryId, "fulfilled");
+ }
+
+ markExpired(registryEntryId: string): void {
+ this._transition(registryEntryId, "expired");
+ }
+
+ withdraw(registryEntryId: string, filerAddress: string): void {
+ const entry = this._get(registryEntryId);
+ if (entry.filerAddress !== filerAddress) {
+ throw new Error("Only the filer may withdraw an intent");
+ }
+ if (entry.status !== "active") {
+ throw new Error(`Cannot withdraw intent with status '${entry.status}'`);
+ }
+ this._transition(registryEntryId, "withdrawn");
+ }
+
+ // ---------------------------------------------------------------------------
+ // Breach Detection
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Compare declared intent params against actual observed params.
+ * Returns a deviation record if a breach is detected, null if compliant.
+ */
+ detectBreach(
+ registryEntryId: string,
+ actual: {
+ capability?: string;
+ params?: Record;
+ bondUsed?: number;
+ railUsed?: string;
+ }
+ ): DeviationRecord | null {
+ const entry = this._get(registryEntryId);
+ const intent = entry.intent;
+
+ // Capability mismatch
+ if (actual.capability && actual.capability !== intent.capability) {
+ return {
+ type: "capability_mismatch" as BreachType,
+ declared: intent.capability,
+ actual: actual.capability,
+ };
+ }
+
+ // Bond violation (agent used more than declared maxBond)
+ if (actual.bondUsed !== undefined && actual.bondUsed > intent.maxBond) {
+ return {
+ type: "bond_violation" as BreachType,
+ declared: intent.maxBond,
+ actual: actual.bondUsed,
+ deviationMagnitude: (actual.bondUsed - intent.maxBond) / intent.maxBond,
+ };
+ }
+
+ // Rail switch
+ if (actual.railUsed && actual.railUsed !== intent.rail) {
+ return {
+ type: "rail_switch" as BreachType,
+ declared: intent.rail,
+ actual: actual.railUsed,
+ };
+ }
+
+ // Param deviation — check each declared param against actual
+ if (actual.params && intent.params) {
+ for (const [key, declaredVal] of Object.entries(intent.params)) {
+ const actualVal = actual.params[key];
+ if (actualVal === undefined) continue;
+ if (typeof declaredVal === "number" && typeof actualVal === "number") {
+ const magnitude = Math.abs(actualVal - declaredVal) / (Math.abs(declaredVal) || 1);
+ if (magnitude > 0.10) { // >10% deviation triggers breach
+ return {
+ type: "param_deviation" as BreachType,
+ declared: { [key]: declaredVal },
+ actual: { [key]: actualVal },
+ deviationMagnitude: magnitude,
+ };
+ }
+ } else if (String(actualVal) !== String(declaredVal)) {
+ return {
+ type: "param_deviation" as BreachType,
+ declared: { [key]: declaredVal },
+ actual: { [key]: actualVal },
+ };
+ }
+ }
+ }
+
+ return null; // no breach detected
+ }
+
+ /**
+ * File a formal breach against an active entry.
+ */
+ fileBreach(breach: Omit): PoIBreach {
+ const entry = this._get(breach.registryEntryId);
+ if (entry.status !== "active" && entry.status !== "fulfilled") {
+ throw new Error(`Cannot file breach against entry with status '${entry.status}'`);
+ }
+ if (!breach.evidence || breach.evidence.length === 0) {
+ throw new Error("At least one evidence item is required to file a breach");
+ }
+
+ const breachId = randomUUID();
+ const fullBreach: PoIBreach = {
+ "@context": "https://zeroquery.dev/ns/poi/v1",
+ "@type": "PoIBreach",
+ ...breach,
+ };
+
+ this._breaches.set(breachId, fullBreach);
+ this._transition(breach.registryEntryId, "breached");
+ entry.breachId = breachId;
+ this._audit(breach.registryEntryId, "breach_filed", {
+ breachId,
+ filedBy: breach.filedBy,
+ deviationType: breach.deviation.type,
+ });
+
+ return fullBreach;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Query
+ // ---------------------------------------------------------------------------
+
+ getEntry(registryEntryId: string): PoIRegistryEntry {
+ return this._get(registryEntryId);
+ }
+
+ getBreach(breachId: string): PoIBreach {
+ const b = this._breaches.get(breachId);
+ if (!b) throw new Error(`Breach ${breachId} not found`);
+ return b;
+ }
+
+ query(opts: RegistryQueryOptions = {}): PoIRegistryEntry[] {
+ const {
+ filerAddress, capability, status, since, until,
+ limit = 100, offset = 0,
+ } = opts;
+
+ let candidates: PoIRegistryEntry[];
+
+ if (filerAddress) {
+ const ids = this._byFiler.get(filerAddress) ?? new Set();
+ candidates = [...ids].map(id => this._entries.get(id)!).filter(Boolean);
+ } else {
+ candidates = [...this._entries.values()];
+ }
+
+ if (capability) candidates = candidates.filter(e => e.intent.capability === capability);
+ if (status) candidates = candidates.filter(e => e.status === status);
+ if (since) candidates = candidates.filter(e => e.receivedAt >= since);
+ if (until) candidates = candidates.filter(e => e.receivedAt <= until);
+
+ candidates.sort((a, b) => b.receivedAt - a.receivedAt);
+ return candidates.slice(offset, offset + limit);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Audit Trail (court-admissible output)
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Return the full audit trail for an entry, including a SHA-256 hash
+ * of the entire trail for court-admissible verification.
+ */
+ getAuditTrail(registryEntryId: string): {
+ registryEntryId: string;
+ entry: PoIRegistryEntry;
+ events: AuditTrailEntry[];
+ trailHash: string;
+ exportedAt: number;
+ } {
+ const entry = this._get(registryEntryId);
+ const events = this._auditTrail.get(registryEntryId) ?? [];
+ const trailPayload = JSON.stringify({ registryEntryId, entry, events });
+ const trailHash = createHash("sha256").update(trailPayload).digest("hex");
+
+ return {
+ registryEntryId,
+ entry,
+ events,
+ trailHash,
+ exportedAt: unixNow(),
+ };
+ }
+
+ stats(): { totalEntries: number; totalBreaches: number; byStatus: Record } {
+ const byStatus: Record = {};
+ for (const entry of this._entries.values()) {
+ byStatus[entry.status] = (byStatus[entry.status] ?? 0) + 1;
+ }
+ return { totalEntries: this._entries.size, totalBreaches: this._breaches.size, byStatus };
+ }
+
+ // ---------------------------------------------------------------------------
+ // Private helpers
+ // ---------------------------------------------------------------------------
+
+ private _get(id: string): PoIRegistryEntry {
+ const entry = this._entries.get(id);
+ if (!entry) throw new Error(`Registry entry ${id} not found`);
+ return entry;
+ }
+
+ private _transition(id: string, status: RegistryStatus): void {
+ const entry = this._get(id);
+ const prev = entry.status;
+ entry.status = status;
+ entry.resolvedAt = unixNow();
+ this._audit(id, "status_changed", { from: prev, to: status });
+ }
+
+ private _audit(id: string, event: AuditTrailEntry["event"], detail: Record): void {
+ if (!this._auditTrail.has(id)) this._auditTrail.set(id, []);
+ this._auditTrail.get(id)!.push({ timestamp: unixNow(), event, detail });
+ }
+}
+
+function unixNow(): number {
+ return Math.floor(Date.now() / 1000);
+}
diff --git a/packages/intent-registry/src/types.ts b/packages/intent-registry/src/types.ts
new file mode 100644
index 0000000..0f4b704
--- /dev/null
+++ b/packages/intent-registry/src/types.ts
@@ -0,0 +1,103 @@
+/**
+ * ZeroQuery Intent Registry — type definitions.
+ * Mirrors the JSON schemas in /schema/ as TypeScript interfaces.
+ */
+
+export type Rail = "usdc-sol" | "rlusd-xrp" | "xrp" | "usdc-base";
+export type RegistryStatus = "active" | "fulfilled" | "breached" | "expired" | "withdrawn";
+export type BreachType =
+ | "capability_mismatch"
+ | "param_deviation"
+ | "bond_violation"
+ | "rail_switch"
+ | "undeclared_action"
+ | "timeout";
+
+export type EvidenceType =
+ | "xrpl_tx"
+ | "api_log"
+ | "mcp_call"
+ | "on_chain_record"
+ | "signed_attestation";
+
+export interface PoIIntent {
+ "@context": "https://zeroquery.dev/ns/poi/v1";
+ "@type": "PoIIntent";
+ capability: string;
+ params: Record;
+ maxBond: number;
+ rail: Rail;
+ id?: string;
+ ttlSeconds?: number;
+ nonce?: string;
+}
+
+export interface XrplAnchor {
+ txHash: string;
+ ledgerIndex: number;
+ closeTime: number;
+ account: string;
+}
+
+export interface PoIRegistryEntry {
+ "@context": "https://zeroquery.dev/ns/poi/v1";
+ "@type": "PoIRegistryEntry";
+ registryEntryId: string;
+ intentId: string;
+ filerAddress: string;
+ receivedAt: number;
+ intentHash: string;
+ xrplAnchor?: XrplAnchor;
+ status: RegistryStatus;
+ resolvedAt?: number;
+ breachId?: string;
+ intent: PoIIntent;
+}
+
+export interface EvidenceItem {
+ type: EvidenceType;
+ ref: string;
+ description?: string;
+}
+
+export interface DeviationRecord {
+ type: BreachType;
+ declared: unknown;
+ actual: unknown;
+ deviationMagnitude?: number;
+}
+
+export interface Remedy {
+ type: "bond_slash" | "rlusd_penalty" | "service_suspension" | "arbitration";
+ amountRLUSD?: number;
+ narrative?: string;
+}
+
+export interface PoIBreach {
+ "@context": "https://zeroquery.dev/ns/poi/v1";
+ "@type": "PoIBreach";
+ intentId: string;
+ registryEntryId: string;
+ filedBy: string;
+ filedAt: number;
+ deviation: DeviationRecord;
+ evidence: EvidenceItem[];
+ remedy?: Remedy;
+ signature?: string;
+}
+
+export interface RegistryQueryOptions {
+ filerAddress?: string;
+ capability?: string;
+ status?: RegistryStatus;
+ since?: number;
+ until?: number;
+ limit?: number;
+ offset?: number;
+}
+
+export interface AuditTrailEntry {
+ timestamp: number;
+ event: "registered" | "status_changed" | "breach_filed" | "anchor_set";
+ detail: Record;
+}
diff --git a/packages/intent-registry/test/registry.test.js b/packages/intent-registry/test/registry.test.js
new file mode 100644
index 0000000..8266783
--- /dev/null
+++ b/packages/intent-registry/test/registry.test.js
@@ -0,0 +1,116 @@
+/**
+ * Basic smoke tests for the ZeroQuery IntentRegistry.
+ * Run with: node --test test/registry.test.js
+ */
+
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+
+import { IntentRegistry } from "../dist/index.js";
+
+const FILER = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
+
+const baseIntent = () => ({
+ "@context": "https://zeroquery.dev/ns/poi/v1",
+ "@type": "PoIIntent",
+ capability: "travel.hotel.search",
+ params: { city: "NYC", maxPrice: 200 },
+ maxBond: 1000,
+ rail: "rlusd-xrp",
+});
+
+describe("IntentRegistry", () => {
+ it("registers an intent and returns a registry entry", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ assert.equal(entry["@type"], "PoIRegistryEntry");
+ assert.equal(entry.status, "active");
+ assert.equal(entry.filerAddress, FILER);
+ assert.ok(entry.intentHash.length === 64, "SHA-256 hex hash is 64 chars");
+ assert.ok(entry.registryEntryId, "has registryEntryId");
+ });
+
+ it("retrieves entry by id", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ const fetched = reg.getEntry(entry.registryEntryId);
+ assert.equal(fetched.intentHash, entry.intentHash);
+ });
+
+ it("detects capability mismatch breach", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ const dev = reg.detectBreach(entry.registryEntryId, {
+ capability: "finance.stock.buy",
+ });
+ assert.ok(dev, "breach detected");
+ assert.equal(dev.type, "capability_mismatch");
+ assert.equal(dev.declared, "travel.hotel.search");
+ assert.equal(dev.actual, "finance.stock.buy");
+ });
+
+ it("detects bond violation", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ const dev = reg.detectBreach(entry.registryEntryId, { bondUsed: 1500 });
+ assert.ok(dev);
+ assert.equal(dev.type, "bond_violation");
+ });
+
+ it("returns null when compliant", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ const dev = reg.detectBreach(entry.registryEntryId, {
+ capability: "travel.hotel.search",
+ bondUsed: 999,
+ railUsed: "rlusd-xrp",
+ params: { city: "NYC", maxPrice: 205 }, // within 10%
+ });
+ assert.equal(dev, null);
+ });
+
+ it("files a formal breach and transitions status", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ const breach = reg.fileBreach({
+ intentId: entry.intentId,
+ registryEntryId: entry.registryEntryId,
+ filedBy: "rPVMhWBsfF9iMXYj3aAzJVkPDTFNSyWdKy",
+ filedAt: Math.floor(Date.now() / 1000),
+ deviation: { type: "capability_mismatch", declared: "travel.hotel.search", actual: "finance.stock.buy" },
+ evidence: [{ type: "api_log", ref: "https://example.com/log/abc123" }],
+ });
+ assert.equal(breach["@type"], "PoIBreach");
+ const updated = reg.getEntry(entry.registryEntryId);
+ assert.equal(updated.status, "breached");
+ });
+
+ it("emits an audit trail with trailHash", () => {
+ const reg = new IntentRegistry();
+ const entry = reg.register(baseIntent(), FILER);
+ reg.markFulfilled(entry.registryEntryId);
+ const trail = reg.getAuditTrail(entry.registryEntryId);
+ assert.ok(trail.events.length >= 2); // registered + status_changed
+ assert.ok(trail.trailHash.length === 64);
+ assert.equal(trail.entry.status, "fulfilled");
+ });
+
+ it("queries by filer address", () => {
+ const reg = new IntentRegistry();
+ reg.register(baseIntent(), FILER);
+ reg.register({ ...baseIntent(), capability: "finance.market.scan" }, FILER);
+ const results = reg.query({ filerAddress: FILER });
+ assert.equal(results.length, 2);
+ });
+
+ it("filters query by status", () => {
+ const reg = new IntentRegistry();
+ const e1 = reg.register(baseIntent(), FILER);
+ reg.register(baseIntent(), FILER);
+ reg.markFulfilled(e1.registryEntryId);
+ const active = reg.query({ status: "active" });
+ const fulfilled = reg.query({ status: "fulfilled" });
+ assert.equal(fulfilled.length, 1);
+ assert.ok(active.length >= 1);
+ });
+});
diff --git a/packages/intent-registry/tsconfig.json b/packages/intent-registry/tsconfig.json
new file mode 100644
index 0000000..544367b
--- /dev/null
+++ b/packages/intent-registry/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "types": ["node"],
+ "strict": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "esModuleInterop": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "test"]
+}
diff --git a/packages/relay/src/relay.ts b/packages/relay/src/relay.ts
index 4fb8907..0c6885d 100644
--- a/packages/relay/src/relay.ts
+++ b/packages/relay/src/relay.ts
@@ -10,7 +10,7 @@
* this package has a single dependency — the protocol SDK — and no network or
* proprietary code.
*/
-import { isExpired, isValidDid, calculateIntentRank, resolveDid, type GossipMessage, type LedgerStateReader } from "@zeroquery/sdk";
+import { isExpired, isValidDid, calculateIntentRank, resolveDid, PAYMENT_RAILS, type GossipMessage, type LedgerStateReader } from "@zeroquery/sdk";
export interface Peer {
id: string;
@@ -168,7 +168,10 @@ export class RelayNode {
msg.bondAmount > 0 &&
Number.isInteger(msg.timestamp) &&
Number.isInteger(msg.ttl) &&
- msg.ttl > 0
+ msg.ttl > 0 &&
+ // Validate that paymentRail is one of the spec-defined rails (spec §3.6).
+ // An unrecognized rail value indicates a malformed or spoofed message.
+ (PAYMENT_RAILS as readonly string[]).includes(msg.paymentRail)
);
}
}
diff --git a/packages/sdk/src/intent.ts b/packages/sdk/src/intent.ts
index d78d05c..43a5775 100644
--- a/packages/sdk/src/intent.ts
+++ b/packages/sdk/src/intent.ts
@@ -46,6 +46,13 @@ export interface GossipMessage {
export const INTENT_CONTEXT =
"https://zeroquery.dev/ns/poi/v1";
+/**
+ * Maximum serialized byte size of an intent payload (spec §4.1 — the gossip
+ * wire message must stay compact to bound relay memory and network cost).
+ * Payloads exceeding this limit are rejected by `validateIntentPayload`.
+ */
+export const MAX_INTENT_PAYLOAD_BYTES = 65_536; // 64 KiB
+
/**
* Deterministic JSON canonicalization (sorted keys, no insignificant
* whitespace). Good enough for a stable hash without pulling in a full
@@ -108,6 +115,17 @@ export function buildGossipMessage(args: BuildGossipArgs): GossipMessage {
/** Returns a list of human-readable validation errors ([] === valid). */
export function validateIntentPayload(payload: IntentPayload): string[] {
const errors: string[] = [];
+
+ // Guard against DoS via oversized payloads before any field inspection.
+ // The limit (MAX_INTENT_PAYLOAD_BYTES) is chosen to keep relay memory
+ // bounded while accommodating realistic intent parameter objects.
+ const serialized = JSON.stringify(payload);
+ if (typeof serialized === "string" && Buffer.byteLength(serialized, "utf8") > MAX_INTENT_PAYLOAD_BYTES) {
+ errors.push(`payload exceeds maximum size of ${MAX_INTENT_PAYLOAD_BYTES} bytes`);
+ // Return early — further validation is meaningless for an oversize payload.
+ return errors;
+ }
+
if (payload?.["@context"] !== INTENT_CONTEXT) {
errors.push(`@context must be "${INTENT_CONTEXT}"`);
}
diff --git a/packages/sdk/src/resolver.ts b/packages/sdk/src/resolver.ts
index 3b0e29d..a881184 100644
--- a/packages/sdk/src/resolver.ts
+++ b/packages/sdk/src/resolver.ts
@@ -183,11 +183,16 @@ export class XahauJsonRpcReader implements LedgerStateReader {
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`xahau node ${res.status}`);
- const json: any = await res.json();
- const node = json?.result?.node;
- if (!node || json?.result?.error) return null;
- const dataHex: string | undefined = node.HookStateData;
- if (!dataHex) return null;
+ const json: unknown = await res.json();
+ if (typeof json !== "object" || json === null) return null;
+ const result = (json as Record)["result"];
+ if (typeof result !== "object" || result === null) return null;
+ const resultObj = result as Record;
+ if (resultObj["error"]) return null;
+ const node = resultObj["node"];
+ if (typeof node !== "object" || node === null) return null;
+ const dataHex = (node as Record)["HookStateData"];
+ if (typeof dataHex !== "string") return null;
return new Uint8Array(Buffer.from(dataHex, "hex"));
}
}
diff --git a/packages/sdk/src/verifier.ts b/packages/sdk/src/verifier.ts
index 624eb96..219d924 100644
--- a/packages/sdk/src/verifier.ts
+++ b/packages/sdk/src/verifier.ts
@@ -1,19 +1,44 @@
-import { createHash } from "node:crypto";
-// In a full solana/web3.js integration, we'd use PublicKey.findProgramAddressSync.
-// For the SDK which aims to be light and unopinionated (as seen in dust.ts/did.ts),
-// we provide the deterministic derivation logic.
+/**
+ * ZK verifier helpers for the poi-verifier Solana program. (spec §3.5)
+ *
+ * The `poi-verifier` program owns a PDA (`verifier_authority`) that acts as the
+ * `verifier` signer for `poi-escrow` bonds. Off-chain provers must know this
+ * PDA's address before constructing the CPI transaction.
+ *
+ * NOTE: `getVerifierPda()` below is a compile-time placeholder. In production,
+ * use `@solana/web3.js` `PublicKey.findProgramAddressSync` with the seed
+ * `[b"verifier_authority"]` against `POI_VERIFIER_PROGRAM_ID`.
+ */
+/** On-chain program ID of the poi-verifier Solana program (Phase 2 scaffold). */
export const POI_VERIFIER_PROGRAM_ID = "Verif1er11111111111111111111111111111111111";
/**
- * Returns the Verifier PDA string and bump.
- * This PDA is used as the `verifier` in `poi-escrow` bonds for ZK attestation.
+ * Returns the deterministic Verifier PDA address and bump seed.
+ *
+ * The PDA is derived from the seed `["verifier_authority"]` under
+ * `POI_VERIFIER_PROGRAM_ID`. It is stored as the `verifier` field when
+ * opening a bond so the `poi-verifier` program can sign CPI calls to
+ * `poi-escrow::fulfill` / `poi-escrow::slash` without exposing a private key.
+ *
+ * @returns An object with `pda` (base58 address) and `bump` (canonical bump).
+ *
+ * @remarks
+ * This is a SDK-layer placeholder that returns a deterministic mock value.
+ * In a live integration, call:
+ * ```ts
+ * import { PublicKey } from "@solana/web3.js";
+ * const [pda, bump] = PublicKey.findProgramAddressSync(
+ * [Buffer.from("verifier_authority")],
+ * new PublicKey(POI_VERIFIER_PROGRAM_ID),
+ * );
+ * ```
*/
export function getVerifierPda(): { pda: string; bump: number } {
- // In a real implementation this would use PublicKey.findProgramAddressSync
- // For the SDK placeholder we return a mock structure.
+ // Placeholder — replace with PublicKey.findProgramAddressSync in a full
+ // @solana/web3.js integration (see JSDoc above).
return {
pda: "ZKPda11111111111111111111111111111111111111",
- bump: 255
+ bump: 255,
};
}
diff --git a/packages/zk-circuits/Cargo.toml b/packages/zk-circuits/Cargo.toml
new file mode 100644
index 0000000..f91f3e8
--- /dev/null
+++ b/packages/zk-circuits/Cargo.toml
@@ -0,0 +1,7 @@
+# Separate Cargo workspace for ZK circuits.
+# sp1-sdk requires zeroize ^1.7 while anchor-lang requires zeroize <1.4.
+# These two ecosystems are incompatible in a single workspace, so the ZK
+# circuits live in their own workspace and are checked by a separate CI job.
+[workspace]
+members = ["program", "script"]
+resolver = "2"
diff --git a/packages/zk-circuits/program/Cargo.toml b/packages/zk-circuits/program/Cargo.toml
index 31635c4..91c9def 100644
--- a/packages/zk-circuits/program/Cargo.toml
+++ b/packages/zk-circuits/program/Cargo.toml
@@ -4,7 +4,7 @@ name = "poi-circuit"
edition = "2021"
[dependencies]
-sp1-zkvm = "3.0.0"
+sp1-zkvm = "3.4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
ed25519-dalek = { version = "2.1.0", default-features = false }
diff --git a/packages/zk-circuits/script/Cargo.toml b/packages/zk-circuits/script/Cargo.toml
index 1d2711a..333517a 100644
--- a/packages/zk-circuits/script/Cargo.toml
+++ b/packages/zk-circuits/script/Cargo.toml
@@ -4,7 +4,7 @@ name = "poi-prover"
edition = "2021"
[dependencies]
-sp1-sdk = "3.0.0"
+sp1-sdk = "3.4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread", "macros"] }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 591405d..ddc1aa1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,6 +34,19 @@ importers:
specifier: ^5.4.5
version: 5.9.3
+ packages/intent-registry:
+ dependencies:
+ '@zeroquery/sdk':
+ specifier: workspace:*
+ version: link:../sdk
+ devDependencies:
+ '@types/node':
+ specifier: ^20.12.0
+ version: 20.19.43
+ typescript:
+ specifier: ^5.4.5
+ version: 5.9.3
+
packages/relay:
dependencies:
'@zeroquery/sdk':
diff --git a/programs/poi-escrow/src/lib.rs b/programs/poi-escrow/src/lib.rs
index 24d6c23..4ba3687 100644
--- a/programs/poi-escrow/src/lib.rs
+++ b/programs/poi-escrow/src/lib.rs
@@ -14,11 +14,23 @@
//! the verifier is an oracle/attestation key; in Phase 2 it is replaced by
//! the ZK provenance verifier program (spec §3.5) via CPI — still no human.
//!
+//! COIN ISOLATION (spec §3.6):
+//! The escrow only accepts the single `mint` the broadcaster passes at bond
+//! creation. The vault is seeded with `intent_hash` AND the mint address so
+//! it is impossible for the vault to receive tokens of a different mint than
+//! the one locked at open time. All resolution paths (`fulfill`, `slash`,
+//! `expire`) validate `bond.mint == mint` via the `has_one` constraint before
+//! any transfer executes. The program therefore NEVER holds SPL tokens from
+//! more than one mint per bond, and mixing mints across bonds is structurally
+//! impossible.
+//!
//! Outcomes:
//! fulfill -> vault → responder (verifier attests a valid provenance proof)
//! expire -> vault → broadcaster (anyone may crank once `expiry` passes)
//! slash -> vault → slash_sink (verifier attests a false fulfillment)
+#![deny(unused_must_use)]
+
use anchor_lang::prelude::*;
use anchor_spl::token::{self, CloseAccount, Mint, Token, TokenAccount, Transfer};
diff --git a/programs/poi-gossip/src/lib.rs b/programs/poi-gossip/src/lib.rs
index 487f055..2f1da63 100644
--- a/programs/poi-gossip/src/lib.rs
+++ b/programs/poi-gossip/src/lib.rs
@@ -16,6 +16,8 @@
//! NOTE: build with the Anchor/Solana SBF toolchain (`anchor build`). The pinned
//! versions are in Cargo.toml.
+#![deny(unused_must_use)]
+
use anchor_lang::prelude::*;
use anchor_lang::system_program;
@@ -49,7 +51,12 @@ pub mod poi_gossip {
Ok(())
}
- /// Admin-only: update the treasury or fee.
+ /// Admin-only: update the protocol treasury address or the broadcast fee.
+ ///
+ /// Either field is optional — pass `None` to leave it unchanged.
+ /// Only the `admin` key recorded in `Config` at `initialize` time may call
+ /// this instruction (enforced by the `has_one = admin` constraint on
+ /// [`SetParams`]).
pub fn set_params(
ctx: Context,
new_treasury: Option,
diff --git a/programs/poi-verifier/src/lib.rs b/programs/poi-verifier/src/lib.rs
index 4598f1d..d9fd4a1 100644
--- a/programs/poi-verifier/src/lib.rs
+++ b/programs/poi-verifier/src/lib.rs
@@ -1,3 +1,5 @@
+#![deny(unused_must_use)]
+
use anchor_lang::prelude::*;
use poi_escrow::cpi::accounts::Resolve;
use poi_escrow::program::PoiEscrow;
@@ -10,7 +12,12 @@ declare_id!("Verif1er11111111111111111111111111111111111");
pub mod poi_verifier {
use super::*;
- /// Initialize the global verifier configuration with the ZK prover Image ID.
+ /// One-time initialization: record the ZK prover Image ID and the admin key.
+ ///
+ /// `image_id` is the 32-byte SP1 guest program image ID committed at compile
+ /// time. `submit_proof` will verify incoming proofs against this value in
+ /// Phase 2 (currently a scaffold stub). Only `admin` can update this via a
+ /// future `set_image_id` instruction.
pub fn initialize(ctx: Context, image_id: [u8; 32]) -> Result<()> {
let config = &mut ctx.accounts.config;
config.admin = ctx.accounts.admin.key();
@@ -18,18 +25,33 @@ pub mod poi_verifier {
Ok(())
}
- /// Submit a ZK proof to be verified. On success, it invokes `poi-escrow` to fulfill or slash.
+ /// Submit a ZK proof for on-chain verification; on success CPI-calls `poi-escrow`
+ /// to fulfill or slash the associated bond.
+ ///
+ /// # Arguments
+ /// * `intent_hash` — 32-byte SHA-256 hash of the canonical intent payload.
+ /// * `proof_hash` — 32-byte commitment to the ZK proof (written into the bond record).
+ /// * `outcome` — 1 = fulfill (release bond to responder), 3 = slash (penalise).
+ /// * `proof_data` — Serialised SP1 Groth16/Plonk proof bytes.
+ ///
+ /// # Phase 2 note
+ /// The real implementation must invoke the SP1 Groth16/Plonk on-chain verifier
+ /// against `config.image_id` instead of the non-empty-bytes stub below.
+ /// Until that CPI is wired, any non-empty `proof_data` is accepted — this is
+ /// intentional scaffolding and MUST NOT be deployed to mainnet as-is.
pub fn submit_proof(
ctx: Context,
intent_hash: [u8; 32],
proof_hash: [u8; 32],
outcome: u8, // 1 = fulfill, 3 = slash
- _proof_data: Vec, // The actual SP1 SNARK proof data
+ proof_data: Vec, // The actual SP1 SNARK proof data
) -> Result<()> {
// Step 1: Verify the ZK proof against the stored `config.image_id`.
- // (In a full SP1 integration, we would invoke the SP1 Groth16/Plonk verifier program here).
- // For Phase 2 scaffolding, we mock the success if proof_data is non-empty.
- require!(!_proof_data.is_empty(), VerifierError::InvalidProof);
+ //
+ // PHASE 2 SCAFFOLD: a non-empty byte string stands in for a real SP1
+ // Groth16/Plonk verification CPI against `ctx.accounts.config.image_id`.
+ // Replace this block with the actual verifier CPI before mainnet deployment.
+ require!(!proof_data.is_empty(), VerifierError::InvalidProof);
// Step 2: Sign the CPI using the verifier PDA.
let bump = ctx.bumps.verifier_authority;
diff --git a/schema/breach.schema.json b/schema/breach.schema.json
new file mode 100644
index 0000000..d0ee959
--- /dev/null
+++ b/schema/breach.schema.json
@@ -0,0 +1,92 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://zeroquery.dev/ns/poi/v1/breach.schema.json",
+ "title": "PoIBreach",
+ "description": "Proof-of-Breach declaration (spec §8). Filed when an agent's actual action deviates materially from a registered PoIIntent. Court-admissible when anchored to a registryEntryId with a valid XRPL timestamp.",
+ "type": "object",
+ "required": ["@context", "@type", "intentId", "registryEntryId", "filedBy", "filedAt", "deviation", "evidence"],
+ "additionalProperties": false,
+ "properties": {
+ "@context": { "const": "https://zeroquery.dev/ns/poi/v1" },
+ "@type": { "const": "PoIBreach" },
+ "intentId": {
+ "type": "string",
+ "description": "The id of the original PoIIntent that was breached.",
+ "format": "uri",
+ "minLength": 1
+ },
+ "registryEntryId": {
+ "type": "string",
+ "description": "The registry entry id from the intent registry (contains XRPL anchor txHash).",
+ "minLength": 1
+ },
+ "filedBy": {
+ "type": "string",
+ "description": "XRPL r-address or DID of the party filing the breach claim.",
+ "minLength": 1
+ },
+ "filedAt": {
+ "type": "integer",
+ "description": "Unix timestamp (seconds) when this breach was filed.",
+ "minimum": 0
+ },
+ "deviation": {
+ "type": "object",
+ "description": "Structured comparison of declared vs actual values.",
+ "required": ["type", "declared", "actual"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["capability_mismatch", "param_deviation", "bond_violation", "rail_switch", "undeclared_action", "timeout"],
+ "description": "Category of breach."
+ },
+ "declared": {
+ "description": "The declared value from the original PoIIntent."
+ },
+ "actual": {
+ "description": "The observed value from the on-chain or API record."
+ },
+ "deviationMagnitude": {
+ "type": "number",
+ "description": "For numeric deviations: |actual - declared| / declared (0–1 = percentage deviation)."
+ }
+ },
+ "additionalProperties": false
+ },
+ "evidence": {
+ "type": "array",
+ "description": "Chain of evidence supporting the breach claim.",
+ "minItems": 1,
+ "items": {
+ "type": "object",
+ "required": ["type", "ref"],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["xrpl_tx", "api_log", "mcp_call", "on_chain_record", "signed_attestation"]
+ },
+ "ref": {
+ "type": "string",
+ "description": "XRPL tx hash, URL, or content-addressed hash of the evidence artifact."
+ },
+ "description": { "type": "string" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "remedy": {
+ "type": "object",
+ "description": "Optional requested remedy.",
+ "properties": {
+ "type": { "type": "string", "enum": ["bond_slash", "rlusd_penalty", "service_suspension", "arbitration"] },
+ "amountRLUSD": { "type": "number", "minimum": 0 },
+ "narrative": { "type": "string", "maxLength": 1024 }
+ },
+ "additionalProperties": false
+ },
+ "signature": {
+ "type": "string",
+ "description": "Ed25519 signature over the canonical JSON of this document (excluding this field), base64url-encoded."
+ }
+ }
+}
diff --git a/schema/registry-entry.schema.json b/schema/registry-entry.schema.json
new file mode 100644
index 0000000..46bb09e
--- /dev/null
+++ b/schema/registry-entry.schema.json
@@ -0,0 +1,65 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://zeroquery.dev/ns/poi/v1/registry-entry.schema.json",
+ "title": "PoIRegistryEntry",
+ "description": "An entry in the ZeroQuery intent registry. Wraps a PoIIntent with anchoring metadata: server timestamp, XRPL memo hash, and filer identity. Forms the immutable audit record.",
+ "type": "object",
+ "required": ["@context", "@type", "registryEntryId", "intentId", "filerAddress", "receivedAt", "intentHash", "intent"],
+ "additionalProperties": false,
+ "properties": {
+ "@context": { "const": "https://zeroquery.dev/ns/poi/v1" },
+ "@type": { "const": "PoIRegistryEntry" },
+ "registryEntryId": {
+ "type": "string",
+ "description": "UUID v4 assigned by the registry at ingestion time.",
+ "format": "uuid"
+ },
+ "intentId": {
+ "type": "string",
+ "description": "Mirrors the id field from the wrapped PoIIntent.",
+ "format": "uri"
+ },
+ "filerAddress": {
+ "type": "string",
+ "description": "XRPL r-address that submitted this intent. Used for breach lookup and credit bureau correlation."
+ },
+ "receivedAt": {
+ "type": "integer",
+ "description": "Unix timestamp (seconds) when the registry received and stored this entry.",
+ "minimum": 0
+ },
+ "intentHash": {
+ "type": "string",
+ "description": "SHA-256 hex digest of the canonical JSON of the PoIIntent (before registry wrapping). Immutable content fingerprint."
+ },
+ "xrplAnchor": {
+ "type": "object",
+ "description": "Optional: XRPL transaction that carries this intent hash as a Memo field. Provides court-admissible on-chain timestamp.",
+ "properties": {
+ "txHash": { "type": "string", "description": "XRPL transaction hash (64 hex chars)." },
+ "ledgerIndex": { "type": "integer" },
+ "closeTime": { "type": "integer", "description": "Ledger close time (unix seconds)." },
+ "account": { "type": "string", "description": "Submitting XRPL account." }
+ },
+ "additionalProperties": false
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "fulfilled", "breached", "expired", "withdrawn"],
+ "description": "Lifecycle status of this intent. Updated by breach detection or fulfilment confirmation.",
+ "default": "active"
+ },
+ "resolvedAt": {
+ "type": "integer",
+ "description": "Unix timestamp when status moved from active to a terminal state."
+ },
+ "breachId": {
+ "type": "string",
+ "description": "registryEntryId of the PoIBreach record, if status == breached."
+ },
+ "intent": {
+ "$ref": "https://zeroquery.dev/ns/poi/v1/intent.schema.json",
+ "description": "The full PoIIntent payload, stored verbatim."
+ }
+ }
+}