Developer documentation

Puzzles, difficulty and security

The puzzle

A puzzle is a 32-byte header plus a 32-byte HMAC-SHA256 signature made with the server key.
The header holds the version, the number of zero bits required, the number of sub-puzzles, issue and expiry time, the sitekey id and a random 16-byte puzzle id.

For each sub-puzzle i the browser searches an 8-byte nonce so that SHA-256(header || i || nonce) starts with the required number of zero bits.
Expected work per sub-puzzle is 2^bits hashes;
the eight sub-puzzles smooth out the variance so solving times are predictable.

Difficulty levels

LevelZero bitsExpected hashes
11116,384
21232,768
31365,536
414131,072
515262,144
616524,288
7171,048,576
8182,097,152
9194,194,304
10208,388,608

The default level is set in the .env file (DIFFICULTY_DEFAULT) and can be overridden per sitekey.
A desktop browser computes roughly one to two million hashes per second in the worker: level 4 takes about 0.1 s, level 7 about 1 to 2 s, level 10 about 3 to 8 s.
Phones are slower by a factor of 2 to 5.

Adaptive difficulty and rate limit

The server counts puzzles per client address (as a salted hash) inside a window (RATE_WINDOW, default 10 minutes).
Addresses that cross the thresholds in RATE_STEPS (default 10, 30, 100, 300 puzzles) get one, two, three or four levels more.
Above RATE_PUZZLES_MAX (default 600) the API answers 429.
Shared addresses (offices, carrier NAT) therefore get harder puzzles, never a block, until the hard cap.

What the server verifies

  1. Format and version of the token.
  2. HMAC signature (constant-time comparison).
  3. Expiry (PUZZLE_TTL, default 30 minutes) and a small clock-skew allowance.
  4. The puzzle exists in the database and was not used before.
  5. Every nonce solves its sub-puzzle.
  6. The puzzle is marked as used with an atomic update; a concurrent replay loses.

What is stored

Per puzzle: id, sitekey id, salted address hash, bits, count, issue and expiry time, solved time.
Rows are deleted one hour after expiry during normal API calls (no cron job).
Daily counters per sitekey (issued, verified, rejected) are kept for the statistics;
they contain no personal data.

Limits of a proof of work

A proof of work makes abuse expensive, it does not make it impossible.
A determined attacker with many machines can still solve puzzles.
Combine CDPCaptcha with the usual measures: a rate limit on the form itself, a honeypot field, and plausibility checks of the content.
Our own contact and signup forms do exactly that.