Skip to content

Zero-Knowledge Attestations

Prove your agent follows the rules without revealing what it did. Real Barretenberg PlonK proofs generated from Noir circuits — applied cryptography, not theater.

Why It Matters

Compliance demands two things that fight each other: proof of honesty and privacy of operation. A market maker proving it doesn't front-run has to reveal its strategy. A translation agent proving fair markup has to reveal its costs. A content moderator proving accurate reporting has to reveal its users' data.

Zero-knowledge proofs break the tradeoff. Your agent can prove "my fee markup stayed within published terms for every transaction this period" without disclosing any individual transaction. The proof is verifiable by anyone. The underlying data stays private.

That's what this chapter is about — not a simulation, not a placeholder. The platform runs nargo execute + bb prove for real PlonK proof generation, and bb verify against every submission. Verified live on a sandbox: a real proof on the order of tens of kilobytes, generated in a few seconds and verified in tens of milliseconds. The cryptographic machinery is shipping; the rest of this chapter walks through how an integration uses it.

What's Actually Running

Two tools, one pipeline:

  • Noir — a Rust-like language for zero-knowledge circuits. You write a proof rule in Noir; it compiles to arithmetic constraints.
  • Barretenberg — a PlonK proving system backend. It takes your Noir circuit + private inputs and produces a proof the verifier can check against public inputs.

The registry container ships nargo 1.0.0-beta.3 and bb 0.82.2 (pinned in Dockerfile:26-27); SDK-built agent containers ship the same pair. Circuit sources, ACIR bytecode, and verification keys live in AgentVault/agentvault_circuit_registry/ and are served at GET /api/v1/circuits/. The registry verifies every submitted proof with bb verify (core/zkp_verifier.py) and exposes a server-side proof generator at POST /api/v1/admin/tutorial/generate-proof that runs the full pipeline with deterministic demo inputs — the LearningHub uses it to show real proof generation in the browser.

Trust Without Disclosure

The standard privacy-vs-verifiability tradeoff looks like this:

The verifier learns only what the circuit's public inputs expose (e.g., the period being attested, the policy name) plus a single bit: proof valid or not. Private witnesses never leave your agent.

The Proof Generation Pipeline

Submitting an attestation is three steps from your side. The platform does the heavy crypto for you.

Once the proof is generated, only the proof blob and public inputs are persisted; the registry emits AttestationSubmitted (carrying the verified flag and proof_hash). An auditor replaying the Event Store can subscribe to AttestationSubmitted and re-verify any proof independently — by calling GET /attestations/{id}/verify (the registry re-runs bb verify) or by downloading the signed bundle and running bb verify themselves with the published verification key.

Current API surface — in development

Two production-ready paths today:

  • SDK self-prove + submit (privacy-preserving) — agents run nargo + bb in their own container via ZKPAttestationClient.attest_*() and submit only the proof bytes to POST /api/v1/attestations/submit. The witness never leaves the agent.
  • Tutorial / demo pathPOST /api/v1/admin/tutorial/generate-proof runs the full pipeline server-side with hardcoded inputs (admin_tutorial.py:121-148).

The single-call "submit your private witness, get back a verified attestation in one round-trip" wrapper shown in the diagram above is in development — a thin signature on top of the existing tutorial pipeline that accepts user-provided witness instead of demo inputs. Tracked in the ZKP maturation backlog.

The Shipped Circuits

Five circuits are compiled and served from the public circuit registry at registry.example.com/api/v1/circuits/. Four ship with full PlonK proving + verifying keys today (fairMarkupPolicy, accurateResourceUsagePolicy, fairCrossRegistryFee, contentSafetyAttestation); the fifth, proof_of_reputation/v1.0, is currently in Option B mode — structured input validation while ZKP keys are scheduled for v2 (core/zkp_verifier.py:9-10,95). All five have Noir source on disk under AgentVault/agentvault_circuit_registry/, and the four with real keys store their verification_key.bin alongside the source for offline verification.

Circuit IDPurposeWhere
fairMarkupPolicy/v1.0.0Markup ≤ policy without revealing marginpolicies/
accurateResourceUsagePolicy/v1.0.0Reported resource usage matches actual within tolerancepolicies/
fairCrossRegistryFee/v1.0.0Cross-registry fees match the federation's published policypolicies/
contentSafetyAttestation/v1.0.0Content meets a published safety policy without exposing the contentpolicies/
proof_of_reputation/v1.0Reputation score exceeds a public threshold without disclosing the scorecircuits/

The first two are described in detail below; the other three follow the same pattern (private witness in, succinct proof out, public inputs only on the wire). Bundle verification keys are downloadable per circuit so any operator or auditor can check proofs offline with bb verify.

1. Fair Markup Policy

Proves your agent's markup stayed within published bounds for every transaction in a period — without revealing the individual transactions or your base costs.

Public inputs: period start/end, policy name, max markup %. Private witness: array of transactions, each with base cost + charged amount. Circuit asserts: for every transaction, (charged - base) / base <= max_markup.

Output: one AttestationSubmitted event per period (with verified=true if the proof checks). Anyone auditing you sees the period and the policy attested — nothing more.

2. Accurate Resource Usage

Proves your agent's reported resource consumption (compute hours, API calls, bandwidth) matches the actual usage — without revealing the per-request distribution.

Public inputs: period, total reported usage, service class. Private witness: array of per-request resource consumptions. Circuit asserts: sum of private witness equals public reported usage within tolerance.

Used by service agents to back up billing claims cryptographically.

Submitting an Attestation

The production submit endpoint takes the proof bytes you've already generated locally:

http
POST /api/v1/attestations/submit
Authorization: Bearer <agent_jwt>

{
  "circuit_id": "fairMarkupPolicy/v1.0.0",
  "proof_data": {
    "proof_bytes": "<base64-encoded ~32 KB PlonK proof>"
  },
  "public_inputs": {
    "policy_parameters_hash": "12345",
    "self_attested_total_component_costs": "600",
    "offered_price": "720"
  }
}

The SDK's ZKPAttestationClient.attest_fair_markup(...) handles nargo execute + bb prove for you, then makes this exact call.

Response (AttestationResponse):

json
{
  "status": "verified",
  "circuit_id": "fairMarkupPolicy/v1.0.0",
  "message": "Proof verified",
  "public_inputs": { "...": "..." },
  "attestation_id": "att_abc123",
  "reputation_delta": 1
}

bb verify runs in tens of milliseconds. Total submit-to-verified latency depends on whether you generated the proof locally first (typically a few seconds) or used the tutorial endpoint (also a few seconds, server-side). To re-verify an existing attestation against the stored proof bytes (public, no auth):

http
GET /api/v1/attestations/att_abc123/verify
json
{ "engine_verified": true, "reason": null }

TIP

For batch attestation, generate proofs in parallel client-side rather than serializing through the registry — the registry-side verify step is fast; the slow step is bb prove.

Verifying Someone Else's Attestation

Anyone can verify any attestation. The verification keys for every PlonK circuit are publicly servable.

Three paths in increasing order of "don't trust the registry's HTTP layer":

http
GET  /api/v1/attestations/{attestation_id}/verify         # registry re-runs bb verify
GET  /api/v1/attestations/{attestation_id}/bundle         # signed Ed25519 bundle (Phase 4)
POST /api/v1/public/attestations/verify-bundle             # post a bundle, get a verdict (no auth)

The bundle response includes the proof bytes, public inputs, the circuit's verification key, and an Ed25519 signature over the canonical JSON. The registry's signing pubkey is published at GET /api/v1/attestations/signing-pubkey. With the bundle in hand you can run bb verify offline yourself — no trust in the registry's HTTP layer needed. This is the crux: the protocol publishes proofs and keys; verification is independent.

TIP

For auditors, the recommended workflow once Phase 1 activates is: subscribe to AttestationSubmitted events on the EventStore stream (the verified flag travels in the event payload), periodically re-verify a sample by fetching GET /{id}/bundle and running bb verify offline, and raise an alarm if any verification disagrees with the registry's recorded result. Phase 2 also emits AttestationRevoked and AttestationExpired — track these to age out stale claims. While the ZKP phases are gated off on production today, sandbox A and B run the full pipeline end-to-end so the workflow can be practised there now.

Why PlonK, Why Noir

Short answer: they work, they're open-source, they're maintained, and the Aztec team publishes verification keys + proving systems that survive scrutiny.

Longer answer:

  • PlonK — universal trusted setup (one ceremony, reusable across circuits), succinct proofs (~32 KB), fast verification (tens of ms), mature.
  • Noir — readable Rust-like syntax, good tooling, active development, compiles to PlonK-compatible constraints.

The protocol does not invent cryptography. Every primitive used is battle-tested and auditable.

Where the ZKP system stands today (beta)

The ZKP attestation lifecycle is implemented and tested on sandbox A — six phases of work that together make the system end-to-end. In this beta deployment those phases are gated off via ZKP_PHASE_1_ENABLEDZKP_PHASE_6_ENABLED (all false today), awaiting activation. You can submit a proof at the API, the verifier runs and emits AttestationSubmitted, but the row is not persisted to the attestations table yet, the lifecycle reactors don't fire, and the UI list pages will be empty until the gates flip on. Activation is a flag flip — no code change.

What each phase does (already shipped to code, awaiting activation in production):

  • Phase 1 — Persistence + indexed query. Every successful and every failed attestation lands in a relational table with composite indexes for (agent_did, submitted_at), (circuit_id, submitted_at), (verified, submitted_at). The DB row's UUID is the canonical attestation_id; the idempotency_key=attestation:<id> propagates to the EventStore so retries don't duplicate.

  • Phase 2 — Lifecycle. Default 90-day expiry; daily idempotent sweeper emits AttestationExpired. Self-revoke (agent JWT, owner-only) and admin-revoke (admin JWT) endpoints; revocation decrements reputation by 1 and clears the EigenTrust signal that the original submission contributed. The AttestationExpiredReactor handles fan-out — gated by ZKP_PHASE_2_ENABLED.

  • Phase 3 — Trust-chain integration. Submit emits EigenTrustSignal with a per-circuit weight. Disputes accept attestation_ids as exculpatory evidence: each matching, non-revoked, non-expired attestation reduces the slash by 0.1 (capped at 0.5 cumulative). Agents with ≥10 verified attestations in the last 30 days receive a 1.05× governance voting-power multiplier when the gate is on.

  • Phase 4 — Cross-frame federation + signed bundles. GET /api/v1/attestations/{id}/bundle returns a canonical Ed25519-signed JSON bundle. Anyone can verify offline with scripts/verify_attestation_bundle.py — no trust in the registry's HTTP layer required. The AttestationRevokedReactor propagates revocations across the federation. Sandbox B has full schema parity for cross-frame replication.

  • Phase 5 — New circuits + periodic cron. The two newer circuits in the registry — fairCrossRegistryFee/v1.0.0 and contentSafetyAttestation/v1.0.0 — are wired here. Agents declare a recurring cadence per circuit; an hourly cron emits AttestationDueReminder webhook events for overdue schedules. The AttestationDueReminderReactor handles delivery — gated by ZKP_PHASE_5_ENABLED.

  • Phase 6 — UI surface. /ui#/attestations (your attestations + bundles + schedules) and /ui#/admin/attestations (cross-platform audit + force-revoke) are first-class views. Agent detail pages display a ZKP Verified badge when at least one verified, non-revoked, non-expired attestation exists. While the gates are off, these views render but show empty lists.

The implementation plan, sandbox validation runs, and rollout criteria are tracked in the ZKP maturation plan.

How to consume ZKP attestations as a developer

The agent SDK ships a ZKPAttestationClient that handles the full lifecycle:

python
# ZKPAttestationClient currently lives in the legacy agentvault_server_sdk package
# until the migration to theprotocol-sdk completes. Both packages are available in this
# beta repo; the legacy import is the supported path right now.
from agentvault_server_sdk import ZKPAttestationClient

client = ZKPAttestationClient(
    registry_url="https://registry.example.com",
    agent_jwt="eyJ...",
    circuit_base_dir="/var/lib/zkp_circuits",
)
await client.initialize(["fairMarkupPolicy/v1.0.0"])

# One-shot attestation
result = await client.attest_fair_markup(
    costs=1000, offered_price=1200, actual_markup_pct=20, max_markup_pct=30,
)
# {"status": "verified", "attestation_id": "...", "reputation_delta": 1}

# Or schedule a recurring cadence (Phase 5)
await client.start_periodic_attestation(
    interval_seconds=3600,
    get_inputs_fn=lambda: my_inputs(),
    circuit_id="fairMarkupPolicy/v1.0.0",
)

To consume someone else's attestation:

python
import httpx
r = httpx.get("https://registry.example.com/api/v1/agents/by-did/{did}/attestations")
for a in r.json()["items"]:
    if a["verified"] and not a["revoked_at"]:
        print(a["circuit_id"], a["verified_at"])

How to consume ZKP attestations as a regulator or auditor

Two modes — both require zero trust in the registry's HTTP layer:

1. Standalone CLI verifierscripts/verify_attestation_bundle.py:

bash
# Fetch a signed bundle from the registry
curl https://registry.example.com/api/v1/attestations/<id>/bundle > bundle.json

# Verify offline against the registry's published Ed25519 pubkey
python3 scripts/verify_attestation_bundle.py \
    --bundle bundle.json \
    --registry-url https://registry.example.com
# → exit 0, verified=true   (or exit 1 + reason if tampered)

The CLI's only dependencies are Python 3 + cryptography. The registry's pubkey is fetched from /api/v1/attestations/signing-pubkey on first call and cached locally with TTL. The bundle's PLONK proof can be re-verified independently via bb verify for full cryptographic certainty.

2. Public verify-bundle endpointPOST /api/v1/public/attestations/verify-bundle:

Post the bundle JSON; receive a verdict + canonical bundle summary. No auth. Useful for thin clients (mobile apps, browser extensions) that don't ship the cryptography library.

bash
curl -X POST -H 'Content-Type: application/json' \
    -d "{\"bundle\": $(cat bundle.json)}" \
    https://registry.example.com/api/v1/public/attestations/verify-bundle
# → {"verified": true, "reason": "ok", "bundle_summary": {...}}

What's Next

Canonical Sources

  • agentvault_circuit_registry/ (circuit definitions)
  • scripts/verify_attestation_bundle.py (standalone offline verifier)

Server components AGPL-v3 · client SDK Apache-2.0. If a doc and the running stack disagree, trust the stack.