Skip to main content

Easy PoW for Web Apps

A defense for web services against bots and other spam. A proof-of-work CAPTCHA asks a browser to perform a small amount of computation before a request is accepted. This can be used to reduce automated abuse by making high-volume requests more costly to them. EasyPoW is intended for Python backends that issue proof-of-work challenges to browser clients and verify the returned solutions.

uv add easypow

The basic flow is that the server creates a challenge, that is sent to client, and the client responds with the solution, and after validation we allow their original request to proceed.

Python backend

import easypow

# Within your WebSocket handler:
challenge = easypow.generate(5.0)      # ~seconds to solve
await ws.send_text(challenge)
# Client runs JS:                        solution = await solve(challenge)
solution = await ws.receive_text()
easypow.validate(challenge, solution)  # raises ValueError if invalid

The same can be adapted to any protocol you have, of course, while maintaining some way of keeping the challenge on server, making sure that clients cannot use the same one more than once. WebSockets are particularly useful because we can simply keep challenge in a local variable and do the dance only when the socket is first connected, and then continuing without any further verifications through the ordinary chat your application would do.

Also useful is to start calculating in the background, and submit a response with the next command that requires PoW. This way the verification can happen without disturbing the user, if he spends at least that time filling up a form or whatever activity it is you wish to protect.

JavaScript solver

Download easypow.js module to your project. If needed programmatically, this is also available from the Python module as easypow.js string or via CLI.

It provides solve both as a named and as the default export:

import solve from './easypow.js'

const solution = await solve(challenge)  // optional AbortSignal as 2nd arg

Plain function version

If it better suits you, the entire solution function can be copied from here to where ever you need it.

async function solve(challenge, signal) {
  const B64URL = { alphabet: 'base64url', omitPadding: true }
  const c = Uint8Array.fromBase64(challenge, B64URL)
  const key = await crypto.subtle.importKey('raw', c, 'PBKDF2', false, ['deriveBits'])
  const salt = new Uint32Array(1)
  const args = { name: 'PBKDF2', salt, iterations: 1 << (c[1] & 0xF), hash: 'SHA-512' }
  const mask = (1 << (c[1] >> 4)) - 1
  const fragments = []
  for (let w=c[0]; w-->0;) {
    if (signal?.aborted) throw new DOMException('PoW operation aborted', 'AbortError')
    do {
      ++salt[0]
    } while (new Uint32Array(await crypto.subtle.deriveBits(args, key, 32))[0] & mask)
    fragments.push(new Uint8Array(salt.buffer).toBase64(B64URL).replace(/A+$/, ''))
  }
  return fragments.join('.')
}

CLI

Run via python -m easypow <command> <args>. Supported commands are generate, validate, solve and js, corresponding to the Python API. No console scripts are installed because this module is mainly intended to be used via Python API by other software that might not want such pollution.

Design principles

Many proof-of-work schemes use an exponential difficulty measure: for example, the number of initial zero bits required in a hash. Each additional bit roughly doubles the expected work, which gives rather coarse control over difficulty. The search is also inherently random and memoryless. If a challenge is expected to take 5 seconds and 10 seconds have already passed without a solution, the expected remaining time is still about 5 seconds. An unlucky solve can therefore take much longer than intended.

To make the work both finer-grained and more predictable, we use smaller rounds and require several of them to be solved. A basic round takes about 0.1 seconds on the target machine, but with large random variation. Requiring 10 independent rounds still gives about 1 second of expected work, while averaging out much of that randomness: the total time is considerably more concentrated around 1 second than a single harder 1-second search. The solution contains the nonce found for each round, and the server verifies them individually.

This also separates two useful controls. The zero-bit requirement determines the asymmetry between solving and verification, while the number of required rounds controls total work approximately linearly. Difficulty can therefore be adjusted in small time increments without weakening the basic solve/verify ratio.

Verification itself must be cheap in two ways. First, invalid submissions must not be able to consume substantial server resources: an attacker can always send arbitrary solutions without doing any work. Second, producing a valid solution must be much more expensive than checking one.

Checking a candidate still requires performing the underlying cryptographic operation, so that operation cannot be made arbitrarily expensive. Conversely, if it is made too cheap and the difficulty is moved entirely into requiring more zero bits, JavaScript overhead and other supporting machinery begin to dominate, making browser performance much worse than native code. The implementation therefore uses PBKDF2-SHA512 through Web Crypto with 128 iterations. That would be extremely low for password hashing, but here it places each trial in a useful range: expensive enough that native cryptographic execution dominates, while still cheap for the server to verify.

With the minimal zero-bit requirement, producing a valid round costs 2048 times as much expected work as verifying one candidate. Because a complete solution consists of a sequence of valid rounds, the server can stop at the first failure. Random or fabricated submissions therefore normally cost only a single cheap verification, while a valid solution requires the client to have paid the full proof-of-work cost. Larger work factors increase this asymmetry further because we require a higher number of zero bits while keeping the round count restrained.

Finally, we wish the function be abortable, while allowing the browser to keep running. AbortSignals produce a useful mechanism for external termination, but checking for the signal is expensive. The current implementation checks this between rounds only, and for that reason also we wish to keep the basic round fairly quick. This scales well from 0.1s to a few minutes expected cost in the current implementation. For the sake of simplicity, we did not implement further break points or infinite scaling.

Metadata

Release files for easypow 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for easypow 0.1.0
File Size Uploaded
easypow-0.1.0.tar.gz 8.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for easypow 0.1.0
File Interpreter ABI Platform
easypow-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 16.1 kB

Release files / easypow-0.1.0.tar.gz

Download URL easypow-0.1.0.tar.gz
Size 8.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cf1a67bac27387ff8a2d5ef9ef4bec6f8979c590a73e9b28504cd611bd6a8b9e
BLAKE2b-256 checksum
How to use checksums
6dff1569f4e6e70124a3eb8267d525856e4945746513d53c0542cb13b553914c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / easypow-0.1.0-py3-none-any.whl

Download URL easypow-0.1.0-py3-none-any.whl
Size 7.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c81a0a32012f3c86c7658d27ee1d06cbb45eb83b6ab689ecaf9deff357a2b243
BLAKE2b-256 checksum
How to use checksums
7b88888f06b630e9c060062279c99cc2c86fe42d9f52f65479078c7cd23a227a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page