cc.me / Proof of work

Proof of work.

A JWT-shaped token that proves its issuer burned CPU (or GPU) time on a specific document. Two base64url segments: the document and a suffix the solver searched for. Anyone can verify it with two SHA-256 computations; finding it costs 2level of them on average.

Format

A token is two segments joined by a dot, like a JWT with one segment missing:

b64u(doc) "." b64u(suffix)

For example, with the document hello world:

aGVsbG8gd29ybGQ.XBCKI7lQ3i4

Level

The level of a token is defined from a double SHA-256:

check = SHA-256(SHA-256(doc) || suffix)

Read check as a 256-bit big-endian integer — the first byte of the digest is the most significant. The token's level is the number of trailing zero bits: the largest L such that the integer is divisible by 2L. A token proves level L when its level is at least L.

The example token above hashes to

0e5598dc7f613025fee4cf09cb8a15160894a7471d112a3b73a9b232f0300000

which ends in five zero hex digits and 3 (binary 0011): exactly 20 trailing zero bits, so it proves any level up to 20.

Each attempt is an independent coin flip: finding level L takes 2L hashes on average, with no shortcut and no memory to trade. Hashing the document once and iterating only the suffix means solving cost does not depend on document size: with an 8-byte suffix the search hashes a fixed 40-byte message, a single SHA-256 block. Verification is two hashes regardless of level.

Rough solving times, using the measured rates from the benchmark below:

16
65,536 hashes — instant anywhere.
20
1 million — 0.2 s in single-threaded JavaScript.
24
16.8 million — 3 s in single-threaded JavaScript, instant on a GPU.
28
268 million — under a minute of JavaScript, 0.13 s on an M5 Pro GPU.
32
4.3 billion — a coffee break in JavaScript, 2 s on that GPU.
36
68.7 billion — half a minute on that GPU.
40
1.1 trillion — 9 minutes on that GPU; pick this only to price out GPUs too.

Documents

The scheme does not interpret the document; the application must make it worthless to replay. Include whatever binds the proof to its use — an audience, an action, a timestamp, a server-issued nonce — and reject tokens whose document you did not expect or have seen before. A proof of work rate-limits; it does not authenticate.

Choose levels from the attacker's hardware, not the visitor's: SHA-256 runs 400–1,500× faster on one GPU than on one browser thread (see below), and Bitcoin ASICs are another five orders of magnitude beyond that.

Solve

Runs entirely in this page, on Web Workers hashing in pure JavaScript. Nothing is sent anywhere.

Verify

Solvers

Reference solvers live in pow/: pow.mjs, a Node CLI whose miner is the same pure-JavaScript SHA-256 this page uses; pow.swift, a Metal compute kernel for Apple GPUs; and pow.c, the same kernel in OpenCL for NVIDIA and AMD GPUs (and anything else with an OpenCL driver). All solve, verify, and benchmark:

node pow.mjs solve 32 'hello world'
swiftc -O pow.swift -o pow && ./pow solve 40 'hello world'
cc -O2 pow.c -lOpenCL -o pow-cl && ./pow-cl solve 40 'hello world'

Measured on an Apple M5 Pro (18 CPU cores, 20-core GPU), 10-second runs:

5.4 MH/s
V8 (Node 24), one thread.
72 MH/s
V8, 18 worker threads.
2,033 MH/s
Metal, one kernel hashing one block per GPU thread — 377× one V8 thread, 28× all of them.
2,031 MH/s
OpenCL, the same kernel on the same GPU through Apple's legacy OpenCL.

And on an AMD Ryzen 9 7950X3D (16 cores, 32 threads) with an NVIDIA GeForce RTX 4090, same 10-second runs:

3.5 MH/s
V8 (Node 24), one thread.
51 MH/s
V8, 32 worker threads.
5,146 MH/s
OpenCL on the RTX 4090 — 1,470× one V8 thread, 100× all of them.