Guides
Running an attestor
The one service that must hold a key. It is small, stateless and replaceable on purpose: anyone can run one, and a program can require several.
#Run one
cd backend && cargo build --release -p pof-attest
POF_ATTEST_KEY=<64 hex chars, your Ed25519 seed> \
POF_ANCHORS=frontend/src/data/anchors.json \
POF_REVOCATIONS_URL=https://<site>/api/revocations \
SOLANA_RPC_URL=https://api.devnet.solana.com \
PORT=8787 ./target/release/pof-attest
curl localhost:8787/v1/pubkey # your key, the verifier build, the key fingerprintOr in a container: docker build -f backend/Dockerfile -t pof-attest . from the repository root (with the demo holder), or backend/Dockerfile.render without it. The hosted one is https://vouch-attest.onrender.com.
#Configuration
| Variable | Default | Effect |
|---|---|---|
POF_ATTEST_KEY | ephemeral | Ed25519 seed, hex. Set it, or the public key changes on every restart. |
POF_ANCHORS | required | The anchor table it trusts. |
POF_AUDIENCE | pof-credit:usdc-pool-1 | The only audience it attests for. |
POF_REVOCATIONS_URL · _FILE | — | Revocation sources; the URL is cached for 10 s. |
SOLANA_RPC_URL | — | The slot for attestation freshness; /v1/attest answers 503 without it. |
POF_DEMO_WORLD | — | Enables the demo holder (feature demo-prover). |
POF_RAIL_TOKEN | — | Bearer token required on rail writes. |
POF_WEBHOOK_URL · _SECRET | — | Signed rail events. |
POF_STORE | memory | Persist the rail registry to a JSON file. |
PORT | 8787 |
#Endpoints
| Route | Does |
|---|---|
| POST /v1/attest | Runs pof-verify on {proof}; signs the 139-byte message if Valid and bound; 422 with the verdict otherwise. |
| POST /v1/verify | The verdict, unsigned. For integrators who want a hosted check. |
| GET /v1/pubkey | Public key, verifier version, verifying-key fingerprint, audience, demo prover on or off. |
| GET /v1/anchor/{h} | The anchor records it trusts at a height. |
| POST /v1/certificates | Rail API: verify an exit certificate under the rail’s policy and registry. |
| POST /v1/certificates/{id}/match | Rail API: read the deposit’s nullifiers from lightwalletd; matched or mismatch. |
| POST /v1/batch | Verify a reserves batch; with attest: true, sign it for pof-reserve. |
| POST /v1/demo/prove | The demo holder (only with POF_DEMO_WORLD). |
| GET /health | Liveness. |
Shapes for every route are in the HTTP API reference.
#Operating it
- Verification runs under a per-core semaphore; when every slot is busy the service answers 429 rather than queueing without bound.
- Request bodies are capped at 64 KiB (2 MiB for batches).
- RPC and revocation-list errors are logged, never returned to callers; the list fails open, keeping the last copy.
- Ship the attestor the same anchor table as your verifiers: it trusts only the anchors it was started with.
- On 0.1 CPU (Render free) one verification takes about 0.3 s, a reserves batch with its attestation about 2 s, and a deposit match about 3.4 s; memory stays near 33 MB.
#Getting on an allowlist
pof-gate and pof-reserve each keep up to eight attestor keys and a threshold. With threshold 2, a receipt needs two distinct allowlisted attestors to have signed the identical message, so one compromised attestor can no longer forge anything. The first initialize must be signed by the program’s upgrade authority, so nobody can claim the config before the deployer; after that the admin sets both with set_attestors.
#What it keeps
Its key and a ten-second cache of the revocation list. It logs verdicts and latencies, never proof bytes, and holds no user data. The rail store holds ids, tags, revealed nullifiers, verdicts and times.