Knock docs

Reading the index…

Run a verifier

knock-verifier is the service that decides whether a knock gets in. Its settings, where to run it, and how your server asks it.

Markdown

knock-verifier is a small Rust service you run beside your site. It gives out challenges and checks knocks for one origin, your site's, and keeps what it has seen in a SQLite file. It reads ENS on Ethereum mainnet and bindings on World Chain through RPCs, needs no keys, and never calls Knock.

If your server runs JavaScript or TypeScript, you don't need the service: @knock-dev/verify runs the same checks inside your server.

Build it

From the root of a checkout of getknock/knock:

Shell
cargo build --release -p knock-verifier
./target/release/knock-verifier

It's one binary, with SQLite built in.

Settings

Everything is an environment variable, and only VERIFIER_ORIGIN usually needs setting.

VariableDefaultWhat it is
VERIFIER_ORIGINhttp://localhost:3001Your site's origin, exactly as browsers send it: https://host or https://host:port, or http://localhost or http://127.0.0.1, with or without a port, for development. Knocks for any other origin fail check 2, and CORS allows only this one.
VERIFIER_ADDR127.0.0.1:8788Where it listens.
VERIFIER_DBknock-verifier.dbIts SQLite file.
VERIFIER_ETH_RPChttps://ethereum-rpc.publicnode.comAn Ethereum mainnet RPC, for ENS.
VERIFIER_WORLD_RPChttps://worldchain-mainnet.g.alchemy.com/publicA World Chain RPC, for bindings.
VERIFIER_MAX_BINDING_DAYS365How old a binding's World ID proof may be, in days. Older bindings fail check 6 as binding_expired.
VERIFIER_STRICToffWhether a binding must be final on World Chain first: off, on (or safe), or finalized. Strict mode explains the choice.
VERIFIER_KNOCK_BINDINGknockThe KnockBinding whose bindings count: knock for Knock's, or the address of a network of your own's.

The default origin is http://localhost:3001, for development, so set yours:

Shell
VERIFIER_ORIGIN=https://club.example \
VERIFIER_DB=/var/lib/knock/verifier.db \
VERIFIER_ETH_RPC=https://your-mainnet-rpc.example \
VERIFIER_WORLD_RPC=https://your-worldchain-rpc.example \
./target/release/knock-verifier

What it checks before it starts

Before it takes a single knock, the verifier makes sure of where it reads from, and stops if anything is off:

  • VERIFIER_ORIGIN is an origin it can check knocks for.
  • VERIFIER_ETH_RPC is chain 1, Ethereum mainnet, and ENS's Universal Resolver is there.
  • VERIFIER_WORLD_RPC is chain 480, World Chain, and the KnockBinding it accepts reports version 3.

The KnockBinding it accepts is Knock's production v3 on World Chain, 0x7748D2787DcB6E6024968edC19fcA13ccAbA1699, built in from contracts/deployments/480.json, unless VERIFIER_KNOCK_BINDING names a network of your own's. Bindings on any other KnockBinding, including staging's and older versions', never count.

Verify from your server

Your page gets the knock, but your server should decide what it means. If the browser asks the verifier and then tells your server "they're in", your server is taking the browser's word for it. Instead, have the browser hand the knock to your server, and your server ask the verifier:

Text
browser -- the knock --> your server -- POST /verify --> verifier
browser <-- in, or why not -- your server <-- the answer --

Keep the verifier on 127.0.0.1, where only your server can reach it, and give the browser two routes on your own origin. In Next.js:

app/api/knock/challenge/route.ts
const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788'

// A challenge for the page, passed through as it is
export async function POST() {
  const r = await fetch(`${VERIFIER}/challenge`, { method: 'POST' })
  return new Response(r.body, { status: r.status, headers: { 'content-type': 'application/json' } })
}
app/api/knock/route.ts
const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788'

export async function POST(req: Request) {
  const { assertion } = await req.json()
  const r = await fetch(`${VERIFIER}/verify`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ assertion, action: 'enter' }),
  })
  const v = await r.json()
  if (!v.ok) return Response.json({ ok: false, check: v.check, error: v.error, why: v.why }, { status: r.status })

  // In: one human, bound on World Chain. Their nullifier is the same on every Knock site.
  const { name, nullifier } = v.knock
  await letIn({ human: nullifier, name }) // your session, vote, claim…
  return Response.json({ ok: true, name })
}

Check 2 compares the origin inside the knock, the one the Door saw, with VERIFIER_ORIGIN. Where the request to /verify comes from doesn't matter, so your server can make it.

From the browser, for trying things out

A page can ask the verifier itself, because the verifier allows CORS from VERIFIER_ORIGIN. It's fine for a demo, or when nothing on your server depends on the answer. Then the verifier must be reachable from the browser, behind your TLS proxy, at an address such as https://knock.club.example.

One verifier per origin

A verifier checks knocks for one origin, because the origin is part of what the Wedgie signs. Run one for each:

  • https://club.example and https://www.club.example are two origins, so two verifiers, or one canonical host.
  • Staging at https://staging.club.example gets its own verifier and its own database.
  • For development, run one with VERIFIER_ORIGIN=http://localhost:5173 or wherever your dev server is.

Its database

The SQLite file at VERIFIER_DB holds four tables:

TableHoldsIf you lose it
challengesEach challenge, its origin, when it was given out and whether it's been used. Expired ones are cleared as new ones are given out.Knocks under way fail check 2. People knock again.
countersThe highest counter seen for each Wedgie's key.No harm: old knocks still fail check 2, since their challenges are gone.
revokedKeys it has seen revoked in ENS, which stay refused even if their records come back.A revoked key could get in again if its owner turned it back on.
entriesFor each action, the humans who've done it, with the name they knocked as and when.Every human can do every action once more.

Back it up if entries matter to you. It's one SQLite file, so one verifier process uses it at a time.

Its RPCs

The default RPCs are free public endpoints. They're good for trying Knock out, but they limit how often you can call them, and the verifier makes several calls for each knock: the mainnet head, the name's five Knock records at the latest block and again 3 blocks deep (unless it read the same ones in the last minute), and the binding's receipt on World Chain. For a real site, use your own endpoints or a paid provider.

Your RPCs are also what the verifier believes. An RPC that lies could show records or a binding that aren't on chain. Use providers you trust, or your own nodes. The verifier never logs an RPC's URL, since URLs often carry keys.

When an RPC can't be reached, the verifier refuses the knock as unavailable, with status 503. It never lets a knock in on a guess.

Limits

LimitValueWhen it's hit
Request body16 KiBRefused at check 1, status 400. A knock is under 1 KiB.
Challenges600 an hour, per processbusy, status 429
Checks600 an hour, per processbusy, status 429
Challenges outstanding10,000 at oncebusy at check 2, status 429

Health and logs

GET /health says what the verifier checks for:

JSON
{
  "binding": "0x7748d2787dcb6e6024968edc19fca13ccaba1699",
  "maxBindingDays": 365,
  "ok": true,
  "origin": "https://club.example",
  "strict": "off"
}

It logs each answer, who got in or which check said no, and nothing about who asked:

Text
INFO let in wedgie.itzraj.eth (counter 12)
INFO refused at V8: already_entered

In your own Rust server

knock-verifier is a thin HTTP layer over the knock-verify crate. If your server is in Rust, you can call the checks directly and skip the extra service.