Introduction

Every web service that gives something away for free, such as an account, a vote, a comment box or a ticket, faces the same problem: programs can ask for it millions of times faster than people can. Passwords and encryption keys do not help, because the attacker is not trying to read a secret. The attacker simply looks like a customer.

In 2003, Luis von Ahn, Manuel Blum, Nicholas Hopper and John Langford proposed a different kind of defense in a paper titled CAPTCHA: Using Hard AI Problems for Security, presented at EUROCRYPT 2003. The name is an acronym for Completely Automated Public Turing test to tell Computers and Humans Apart. Three words in it carry the whole idea:

  • Turing test: the question is the one Alan Turing raised, can you tell a person from a machine by what it answers? (see the Church-Turing thesis for the broader context of his work). A CAPTCHA is the reverse of the usual setup, because a computer is the judge and a human or program is the candidate.
  • Automated: the test is generated and graded without a person in the loop.
  • Public: the algorithm and the source code are known. Only a random seed is secret, as in modern cryptography. Security must come from the difficulty of the underlying problem, not from obscurity.

The authors added an argument that is still worth remembering, the win-win: if a CAPTCHA is never broken, we have a practical way to tell people from programs. If it is broken, then someone has solved a hard problem in artificial intelligence. Either outcome is progress. The history of CAPTCHAs is largely the story of how that second branch kept happening.

Try It

The demo has three parts. First, a gallery of generated distorted-text images: drag the distortion slider and notice how your success rate falls as the image gets harder, which is the human side of the trade. Second, a proof-of-work puzzle: your browser searches for a nonce whose SHA-256 hash starts with a chosen number of zero bits, and you see the attempts, the time, and the expected 2k2^k work. Third, a cost table compares one human visit with a million bot visits.

<h3 class="h">1. Distorted-text CAPTCHAs</h3>
<p class="hint">Type the five characters. Raise the distortion and see how your own success rate changes. Resistance to machines is described, not measured here.</p>
<canvas id="cv" width="240" height="80" aria-label="Distorted text image"></canvas>
<label class="row">Distortion <b id="dval">3</b>
  <input id="dist" type="range" min="0" max="10" value="3" aria-label="Distortion">
</label>
<div class="row">
  <input id="ans" type="text" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="8" placeholder="Type the text" aria-label="Type the text">
  <button id="chk" type="button">Check</button>
  <button id="nw" type="button" class="ghost">New image</button>
</div>
<div id="msg" class="status"></div>
<div id="resist" class="note"></div>
<table class="tbl" id="stats"></table>

<h3 class="h">2. Proof of work with SHA-256</h3>
<p class="hint">Find a nonce so that SHA-256(prefix + nonce) starts with k zero bits. Each extra bit doubles the expected work, 2^k hashes.</p>
<label class="row">Difficulty k (zero bits) <b id="bval">14</b>
  <input id="bits" type="range" min="4" max="22" value="14" aria-label="Difficulty k (zero bits)">
</label>
<div class="row">
  <button id="solve" type="button">Solve the puzzle</button>
  <button id="stop" type="button" class="ghost" disabled>Stop</button>
</div>
<div class="bar"><div id="fill"></div></div>
<div id="powout" class="mono"></div>
<div id="powsum" class="status"></div>

<h3 class="h">3. Cost of one visit versus a flood</h3>
<label class="row">Attacker speed-up over this browser:
  <select id="spd" aria-label="Attacker speed-up over this browser:">
    <option value="1">1x</option>
    <option value="50">50x</option>
    <option value="1000">1000x</option>
  </select>
</label>
<table class="tbl" id="cost"></table>
<p class="hint">Times use the hash rate measured in your browser. The cost is trivial once, and large only when multiplied by a flood. Native code or GPUs are faster than JavaScript, which is why the speed-up factor is a parameter.</p>
:root { --fg:#1f2933; --bg:#ffffff; --muted:#52606d; --line:#cbd2d9; --accent:#1d4e89; --accent-fg:#ffffff; --ok:#0a7d33; --bad:#b42318; --soft:#eef2f6; }
@media (prefers-color-scheme: dark) {
  :root { --fg:#e4e7eb; --bg:#14181d; --muted:#9aa5b1; --line:#3e4c59; --accent:#6aa5e0; --accent-fg:#0b1118; --ok:#4cc17a; --bad:#ff7b72; --soft:#1f262e; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; font-size: 14px; }
.h { font-size: 1rem; margin: 1rem 0 .3rem; }
.h:first-child { margin-top: 0; }
.hint { color: var(--muted); line-height: 1.45; margin: 0 0 .6rem; }
.note { color: var(--muted); font-size: .85rem; margin: .3rem 0; min-height: 2.4em; line-height: 1.4; }
canvas { display: block; width: 100%; max-width: 240px; height: auto; border: 1px solid var(--line); border-radius: 8px; background: #f4f1e8; }
.row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .5rem 0; }
input[type=range] { flex: 1 1 120px; min-width: 100px; }
input[type=text] { flex: 1 1 100px; min-width: 90px; font: 16px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); }
select { font: inherit; padding: .3rem; background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg); border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .45; cursor: default; }
.status { font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
.tbl { border-collapse: collapse; width: 100%; font-size: .85rem; margin: .4rem 0; }
.tbl th, .tbl td { border: 1px solid var(--line); padding: .3rem .4rem; text-align: left; }
.tbl th { background: var(--soft); }
.bar { height: 8px; background: var(--soft); border-radius: 4px; overflow: hidden; margin: .4rem 0; }
#fill { height: 100%; width: 0; background: var(--accent); }
.mono { font: 12px ui-monospace, monospace; word-break: break-all; color: var(--muted); min-height: 2.6em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const $ = (id) => document.getElementById(id);

const ALPHA = 'abcdefghjkmnpqrstuvwxyz23456789';
const FONTS = ['serif', 'sans-serif', 'monospace'];
let answer = '';
const stats = { low: [0, 0], mid: [0, 0], high: [0, 0] };
const bucket = (d) => (d <= 3 ? 'low' : d <= 7 ? 'mid' : 'high');
const rnd = (a, b) => a + Math.random() * (b - a);

function newText() {
  let s = '';
  for (let i = 0; i < 5; i++) s += ALPHA[Math.floor(Math.random() * ALPHA.length)];
  return s;
}

function draw(text, d) {
  const cv = $('cv');
  const W = cv.width, H = cv.height;
  const off = document.createElement('canvas');
  off.width = W; off.height = H;
  const g = off.getContext('2d');
  g.fillStyle = '#f4f1e8';
  g.fillRect(0, 0, W, H);
  const step = 42 - d * 1.9;
  const x0 = (W - step * (text.length - 1)) / 2;
  g.textAlign = 'center';
  g.textBaseline = 'middle';
  for (let i = 0; i < text.length; i++) {
    g.save();
    g.translate(x0 + i * step + rnd(-d * 0.6, d * 0.6), H / 2 + rnd(-d * 1.4, d * 1.4));
    g.rotate(rnd(-1, 1) * d * 0.06);
    const sc = 1 + rnd(-1, 1) * d * 0.03;
    g.scale(sc, sc);
    g.font = 'bold ' + Math.round(rnd(34, 40)) + 'px ' + FONTS[Math.floor(Math.random() * 3)];
    g.fillStyle = 'rgb(' + Math.floor(rnd(20, 90)) + ',' + Math.floor(rnd(20, 90)) + ',' + Math.floor(rnd(40, 110)) + ')';
    g.fillText(text[i], 0, 0);
    g.restore();
  }
  const ctx = cv.getContext('2d');
  ctx.fillStyle = '#f4f1e8';
  ctx.fillRect(0, 0, W, H);
  const amp = d * 0.55, freq = rnd(0.05, 0.11), ph = rnd(0, 6.28);
  for (let x = 0; x < W; x++) {
    ctx.drawImage(off, x, 0, 1, H, x, Math.round(amp * Math.sin(x * freq + ph)), 1, H);
  }
  ctx.lineCap = 'round';
  for (let i = 0; i < d * 1.2; i++) {
    ctx.strokeStyle = 'rgba(' + Math.floor(rnd(30, 120)) + ',' + Math.floor(rnd(30, 120)) + ',' + Math.floor(rnd(30, 120)) + ',0.75)';
    ctx.lineWidth = rnd(1, 1 + d * 0.2);
    ctx.beginPath();
    ctx.moveTo(rnd(0, W), rnd(0, H));
    ctx.bezierCurveTo(rnd(0, W), rnd(0, H), rnd(0, W), rnd(0, H), rnd(0, W), rnd(0, H));
    ctx.stroke();
  }
  for (let i = 0; i < d * 25; i++) {
    ctx.fillStyle = 'rgba(60,60,60,0.5)';
    ctx.fillRect(rnd(0, W), rnd(0, H), 1.5, 1.5);
  }
}

function renderStats() {
  const rows = [['low', "Low (0-3)"], ['mid', "Medium (4-7)"], ['high', "High (8-10)"]];
  let h = '<tr><th>Distortion</th><th>Your tries</th><th>Solved</th></tr>';
  for (const [k, name] of rows) {
    const [n, ok] = stats[k];
    h += '<tr><td>' + name + '</td><td>' + n + '</td><td>' + (n ? Math.round((100 * ok) / n) + '%' : '-') + '</td></tr>';
  }
  $('stats').innerHTML = h;
}

function renderResist() {
  const d = +$('dist').value;
  const k = bucket(d);
  $('resist').textContent = k === 'low' ? "Stated: plain OCR reads this easily, so it stops almost nothing." : k === 'mid' ? "Stated: this is the regime of early-2000s CAPTCHAs; naive character segmentation starts to fail." : "Stated: hard for classical pipelines, but deep networks trained on such images have read comparable text (Goodfellow et al., 2014), and people also fail more often.";
}

function newChallenge() {
  answer = newText();
  draw(answer, +$('dist').value);
  $('ans').value = '';
  $('msg').textContent = '';
  $('msg').className = 'status';
}

function check() {
  const v = $('ans').value.trim().toLowerCase();
  if (!v) return;
  const b = stats[bucket(+$('dist').value)];
  b[0]++;
  if (v === answer) {
    b[1]++;
    $('msg').textContent = "Correct.";
    $('msg').className = 'status ok';
  } else {
    $('msg').textContent = fmt("Not quite. It was: %a%", { a: answer });
    $('msg').className = 'status bad';
  }
  renderStats();
  draw(answer, +$('dist').value);
}

$('dist').addEventListener('input', () => {
  $('dval').textContent = $('dist').value;
  renderResist();
  draw(answer, +$('dist').value);
});
$('chk').addEventListener('click', check);
$('nw').addEventListener('click', newChallenge);
$('ans').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') check();
});

const KK = new Uint32Array(64);
const HH = new Uint32Array(8);
(function initConst() {
  const primes = [];
  for (let n = 2; primes.length < 64; n++) {
    let ok = true;
    for (const p of primes) if (p * p <= n && n % p === 0) { ok = false; break; }
    if (ok) primes.push(n);
  }
  for (let i = 0; i < 64; i++) KK[i] = Math.floor((Math.cbrt(primes[i]) % 1) * 4294967296);
  for (let i = 0; i < 8; i++) HH[i] = Math.floor((Math.sqrt(primes[i]) % 1) * 4294967296);
})();

const BLK = new Uint8Array(64);
const W32 = new Uint32Array(64);
const OUT = new Uint32Array(8);

function sha256Short(str) {
  const len = str.length;
  BLK.fill(0);
  for (let i = 0; i < len; i++) BLK[i] = str.charCodeAt(i);
  BLK[len] = 0x80;
  const bits = len * 8;
  BLK[62] = (bits >>> 8) & 255;
  BLK[63] = bits & 255;
  for (let i = 0; i < 16; i++) {
    W32[i] = (BLK[4 * i] << 24) | (BLK[4 * i + 1] << 16) | (BLK[4 * i + 2] << 8) | BLK[4 * i + 3];
  }
  for (let i = 16; i < 64; i++) {
    const a = W32[i - 15], b = W32[i - 2];
    const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
    const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
    W32[i] = (W32[i - 16] + s0 + W32[i - 7] + s1) | 0;
  }
  let a = HH[0], b = HH[1], c = HH[2], d = HH[3], e = HH[4], f = HH[5], g = HH[6], h = HH[7];
  for (let i = 0; i < 64; i++) {
    const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
    const ch = (e & f) ^ (~e & g);
    const t1 = (h + S1 + ch + KK[i] + W32[i]) | 0;
    const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
    const mj = (a & b) ^ (a & c) ^ (b & c);
    const t2 = (S0 + mj) | 0;
    h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
  }
  OUT[0] = (HH[0] + a) | 0; OUT[1] = (HH[1] + b) | 0; OUT[2] = (HH[2] + c) | 0; OUT[3] = (HH[3] + d) | 0;
  OUT[4] = (HH[4] + e) | 0; OUT[5] = (HH[5] + f) | 0; OUT[6] = (HH[6] + g) | 0; OUT[7] = (HH[7] + h) | 0;
  return OUT;
}

function hex(o) {
  let s = '';
  for (let i = 0; i < 8; i++) s += ('00000000' + (o[i] >>> 0).toString(16)).slice(-8);
  return s;
}

function zeroBits(o) {
  return o[0] !== 0 ? Math.clz32(o[0]) : 32 + Math.clz32(o[1]);
}

let rate = 0;
function calibrate() {
  const t0 = performance.now();
  let n = 0;
  while (performance.now() - t0 < 60) {
    for (let i = 0; i < 2000; i++) sha256Short('calibrate-' + (n + i));
    n += 2000;
  }
  rate = n / ((performance.now() - t0) / 1000);
}

function fmtTime(sec) {
  const t = (v, u) => fmt('%v% %u%', { v: v >= 100 ? Math.round(v).toLocaleString() : v.toPrecision(3), u: u });
  if (sec < 1) return t(sec * 1000, "ms");
  if (sec < 60) return t(sec, "s");
  if (sec < 3600) return t(sec / 60, "min");
  if (sec < 86400) return t(sec / 3600, "h");
  if (sec < 31557600) return t(sec / 86400, "days");
  return t(sec / 31557600, "years");
}

function renderCost() {
  const k = +$('bits').value;
  const spd = +$('spd').value;
  const perVisit = Math.pow(2, k) / rate;
  const rows = [
    ["One human visitor", 1, perVisit],
    ["Bot, 1,000 visits", 1000, perVisit / spd],
    ["Bot, 1,000,000 visits", 1000000, perVisit / spd]
  ];
  let h = '<tr><th>Who</th><th>Visits</th><th>CPU time</th></tr>';
  for (const [name, n, per] of rows) {
    h += '<tr><td>' + name + '</td><td>' + n.toLocaleString() + '</td><td>' + fmtTime(per * n) + '</td></tr>';
  }
  $('cost').innerHTML = h;
}

let timer = null;
function stopSolve() {
  if (timer) clearTimeout(timer);
  timer = null;
  $('solve').disabled = false;
  $('stop').disabled = true;
}

function solve() {
  stopSolve();
  const k = +$('bits').value;
  const prefix = 'kipu:' + Math.floor(Math.random() * 1e9).toString(36) + ':';
  let nonce = 0;
  const t0 = performance.now();
  const expected = Math.pow(2, k);
  $('solve').disabled = true;
  $('stop').disabled = false;
  $('powsum').textContent = '';
  $('powsum').className = 'status';
  $('powout').textContent = fmt("Prefix %p% - looking for %k% leading zero bits...", { p: prefix, k: k });
  const step = () => {
    const tEnd = performance.now() + 25;
    while (performance.now() < tEnd) {
      for (let i = 0; i < 500; i++) {
        const o = sha256Short(prefix + nonce);
        if (zeroBits(o) >= k) {
          const secs = (performance.now() - t0) / 1000;
          $('fill').style.width = '100%';
          $('powout').textContent = fmt("Input: %s% - SHA-256: %h%", { s: prefix + nonce, h: hex(o) });
          $('powsum').textContent = fmt("Solved after %n% attempts in %t%. Expected about %e% attempts.", { n: (nonce + 1).toLocaleString(), t: fmtTime(secs), e: expected.toLocaleString() });
          $('powsum').className = 'status ok';
          rate = (nonce + 1) / Math.max(secs, 0.001);
          renderCost();
          stopSolve();
          return;
        }
        nonce++;
      }
    }
    $('fill').style.width = Math.min(99, (100 * nonce) / expected) + '%';
    $('powsum').textContent = fmt("Tried %n% nonces...", { n: nonce.toLocaleString() });
    timer = setTimeout(step, 0);
  };
  step();
}

$('bits').addEventListener('input', () => {
  $('bval').textContent = $('bits').value;
  $('fill').style.width = '0';
  renderCost();
});
$('spd').addEventListener('change', renderCost);
$('solve').addEventListener('click', solve);
$('stop').addEventListener('click', () => {
  stopSolve();
  $('powsum').textContent = "Stopped.";
});

calibrate();
renderStats();
renderResist();
renderCost();
newChallenge();
Result
Console

Two things to notice. In part one the trade-off is uncomfortable: the same distortion that confuses a program also confuses you. In part two the puzzle is not hard for one visitor, but its cost is the same for every visit, so a flood of visits multiplies it. That is the key shift in the history that follows: from "prove you are human" to "pay a small price per request".

The Real Complexity

A cryptographic scheme usually reduces its security to a problem such as factoring (see RSA) or inverting a hash. A CAPTCHA reduces it to a problem in artificial intelligence: reading distorted characters, naming objects in photos, or telling behavior apart. In the language of the original paper, the puzzle is only useful if humans solve it with high probability while no efficient program does much better than chance.

There is no mathematical proof for such a claim, and this is the structural weakness. The assumption is about what current algorithms can do, so it is empirical and it expires. Three milestones show how:

  • Text. In 2008, von Ahn, Maurer, McMillen, Abraham and Blum published reCAPTCHA: Human-Based Character Recognition via Web Security Measures in Science. It reused the effort of solving a CAPTCHA to digitize words that scanners could not read. Google acquired reCAPTCHA in 2009. In 2013 (published at ICLR 2014), Goodfellow and colleagues at Google reported that a deep convolutional network transcribed the hardest category of reCAPTCHA text puzzles with 99.8% accuracy, a result obtained as a by-product of reading house numbers from Street View imagery.
  • Images and audio. Selecting "all squares with traffic lights" asks for object recognition, exactly the task that deep networks improved most. Adversarial examples show the other direction: perturbations that fool a classifier but not a person, which is why image CAPTCHAs also lose some of their margin from that side.
  • Outsourcing humans. A CAPTCHA tests whether a human is present, not whether the human is the one who wants the account. Services that route challenges to low-paid workers turn the test into a priced commodity, so a CAPTCHA acts more like a tax than a wall.

There is also a human cost that this analysis often ignores: distortion that defeats programs also defeats readers with low vision or dyslexia, and audio alternatives are hard for people with hearing impairments and for anyone not fluent in the language spoken.

Proof-of-work has a cleaner model. The idea comes from Dwork and Naor (Pricing via Processing or Combatting Junk Mail, CRYPTO 1992) and Adam Back's Hashcash (announced 1997, described in a 2002 technical report). The server issues a challenge string cc. The client must find a nonce xx such that the first kk bits of H(c ∥ x)H(c \,\|\, x) are zero, where HH is a hash function such as SHA-256.

solve(c, k):                  verify(c, x, k):
  x = 0                         return leading_zero_bits(H(c || x)) >= k
  while leading_zero_bits(H(c || x)) < k:
    x = x + 1
  return x

If the hash behaves like a random function, each attempt succeeds independently with probability p=2−kp = 2^{-k}. The number of attempts NN is geometric:

  • Expected work: E[N]=2k\mathbb{E}[N] = 2^{k} hashes, and the standard deviation is about 2k2^{k} as well, so a single solve varies a lot.
  • Tail: Pr⁡[N>n]=(1−2−k)n≈e−n/2k\Pr[N > n] = (1 - 2^{-k})^{n} \approx e^{-n/2^{k}}. After 3⋅2k3 \cdot 2^{k} attempts the failure probability is about e−3≈5%e^{-3} \approx 5\%.
  • Median: ⌈ln⁡2/(−ln⁡(1−2−k))⌉≈0.69⋅2k\lceil \ln 2 / (-\ln(1-2^{-k})) \rceil \approx 0.69 \cdot 2^{k}.
  • Verification: one hash, O(1)O(1), independent of kk.
  • Memory: O(1)O(1), so the work is purely computational, which is why GPUs and custom chips help the attacker.

The asymmetry between 2k2^k to solve and one hash to check is the same easy to verify, hard to find shape as in one-way functions. It prices requests rather than classifying visitors: with k=20k=20 and a rate of 10610^6 hashes per second, one request costs about 220/106≈12^{20}/10^6 \approx 1 second of CPU, while a million requests cost about 10610^6 seconds, roughly eleven days of CPU. The weakness is the gap between devices: an attacker's native code or GPU is orders of magnitude faster than the JavaScript that an old phone runs, so the difficulty that a phone tolerates is often too low to deter a determined scraper.

Where It Matters

As machine learning closed the gap on perceptual puzzles, the field moved away from asking the user a harder question and toward deciding when to ask at all.

  • Invisible risk scoring. Google announced reCAPTCHA v3 on 29 October 2018. Instead of a challenge, it returns a score for each interaction, and the site decides what to do with it. The signals come from behavior and context, which means the security now depends on data about the visitor, with the privacy cost that implies.
  • Anonymous tokens. Privacy Pass (Davidson, Goldberg, Sullivan, Tankersley and Valsorda, PoPETs 2018; a browser extension appeared in 2017) lets a user who solved one challenge receive a batch of cryptographically unlinkable tokens, so later requests can be authorized without being tracked. The idea was later developed in the IETF.
  • Privacy-preserving checks. Cloudflare Turnstile, announced on 28 September 2022, replaces the visible puzzle with non-interactive browser checks, aimed at removing both the friction and the tracking.
  • Proof-of-work revived. The Anubis filter, released in January 2025 by Xe Iaso (now maintained by Techaro), makes each new visitor solve a SHA-256 proof-of-work challenge before a page loads, as a response to aggressive scraping by AI crawlers. Its own documentation presents it as a blunt tool with side effects, such as blocking well-behaved bots, and in the summer of 2025 Codeberg reported that many scrapers had learned to solve the challenge. The same cost model as in the demo explains both facts: it works until attackers' compute is cheap relative to the value of the data.
  • Cryptocurrencies. Bitcoin's proof-of-work is Hashcash with a global, self-adjusting difficulty, and rate limiting is the complementary defense that bounds requests per client.

No single defense works alone. Real systems combine them: cheap rate limits first, risk scores for the gray zone, a challenge only when needed, and proof-of-work or tokens where privacy matters.

Conclusion

A CAPTCHA turns the Turing test into an engineering component, and the win-win argument was right: the puzzles were eventually broken, and breaking them advanced machine perception. The cost was that the humans became the bottleneck, tired of squinting at letters while the programs got better at them.

What replaced the pure puzzle is more honest: risk scores that spend privacy, anonymous tokens that try to save it, and proof-of-work that makes abuse expensive instead of impossible. None of these asks whether you are human. They ask what it costs to be wrong about you.

If you want to go further, read how SHA-256 is built, how Bitcoin turned the same puzzle into a global consensus mechanism, and how adversarial examples show that machine and human perception still disagree in surprising ways.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/captcha/Content licensed under CC BY-NC 4.0.