The puzzle was hard once and free thereafter
A hypothetical signup service asks clients to solve a hash puzzle. Solving takes effort once. An attacker submits the same proof for a thousand signups, and the server verifies it each time.
The puzzle priced finding a proof. It never priced spending it again.
Make the cost belong to one attempt
Issue a challenge containing a random identifier, action/session binding, expiry, difficulty, and protocol version. Authenticate that exact encoding with a server secret or signature. The client searches for a nonce such that:
For an approximately uniform 256-bit hash, success probability per independent attempt is and expected attempts are . The tail matters: the probability of no success after attempts is . For large , the 95th-percentile effort is roughly , about three times the mean.
If difficulty is 18 bits, is checking that the hex digest starts with four zeros sufficient? What about five zeros?
This runnable helper checks full bytes plus a partial byte. It validates only the bit predicate, not challenge authenticity or replay protection.
pythondef leading_zero_bits(digest: bytes, bits: int) -> bool: if not 0 <= bits <= len(digest) * 8: raise ValueError("difficulty outside digest width") whole, partial = divmod(bits, 8) return (all(byte == 0 for byte in digest[:whole]) and (partial == 0 or digest[whole] >> (8 - partial) == 0))
The executable experiments distinguish 16-, 18-, and 20-bit boundaries and demonstrate one-time challenge spending. Use explicit canonical field framing; ambiguous concatenation or trusting an unsigned client-supplied difficulty defeats the protocol before hashing begins.
Verification is cheap, but not stateless end to end
Validate format/size, authenticated challenge, allowed difficulty, binding, expiry, and nonce proof. Then atomically consume the challenge identity, not (challenge, nonce). Otherwise finding additional valid nonces for one challenge allows repeated use.
pseudocodeverify bounded, canonical challenge fields and authentication check action/session/expiry and allowed difficulty check proof hash predicate atomically claim challenge for this operation intent complete business operation idempotently return stored result on retry
Issuance can be stateless when the signed challenge contains all issuance evidence. Spending still needs durable/appropriate state or another protocol that prevents reuse. Keep spent evidence through the full acceptance window including clock margins. Couple it to the operation so a crash after spending does not permanently strand a legitimate retry; lesson 18 supplies that recovery model.
The attacker's machine is not the user's phone
A Web Worker can move computation off the UI thread, but it still spends battery, CPU, and time. A native/GPU solver may be much faster. Expected hashes are portable mathematics; milliseconds and economic cost are measured properties of hardware and implementation.
Changing difficulty by one bit doubles expected work. A load-based controller can therefore punish legitimate low-end devices rapidly. Bound effort, allow cancellation/expiry, measure tail completion, and offer an alternate path. Memory-hard puzzles change the hardware advantage but also change server verification and device requirements; they do not create a universal human test.
Self-hosting removes a challenge-provider API dependency, not all infrastructure dependencies or personal data. Binding a proof to an IP address creates both privacy considerations and a failure mode when mobile networks change. Prefer a binding appropriate to the operation and threat model.
Observe challenge issue/solve/spend rates, replay rejections, verification CPU, legitimate completion by device class, and the value extracted by accounts that pass. A determined attacker with other motives can still pay the cost. Combine resource admission and benefit controls with the puzzle rather than treating expensive computation as identity.
Mental model: proof of work charges for producing evidence. One-time, scoped consumption connects that expense to each protected action.
Final transfer: choose your boundaries
Design a signup flow that survives a verification timeout, a lost account-creation response, and a worker restart without granting the trial twice. Which earlier mechanisms do you need, and which guarantees would you refuse to claim?
Source trail: 30-hashcash-and-client-side-proof-of-work. See the source map for consolidation and reference-retained material.