Collections

TeikhosBounty

In 2018 Johan Nygren put ETH behind five contracts that pay whoever submits the right public key. Four of them can pay. One did in 2026 with a key a failed attempt had left on chain four years earlier.

In February and March 2018 Johan Nygren deployed five contracts from one address, all verified on Etherscan as TeikhosBounty, and funded each with ETH. The idea behind them: hide a signature behind a hash so nobody can use it until they know the public key. Five days after the last deploy the author proposed it on the EIPs tracker as proof_of_public_key = keccak256(nextPublicKey) and named it Teikhos, Greek for "wall" or "fortification". The contracts are the wall with money behind it.

IDteikhos23 / 265 puzzles · 2018-02-26 → 2018-03-14
Collection

TeikhosBounty

ethereum

Five Ethereum contracts that want a public key nobody published. Four can pay, and one did in 2026, with a key a failed attempt left on chain four years earlier.

Prize recorded
3.5 ETH
Still unsolved
4 · 3 ETH
closed 1 / 5

Census

unsolved
480%
solved
120%
public keys
00%
private keys
00%

Access

LoadgetCollection("teikhos")
Showteikhos/4 in the playground
Listall 5 in the playground

How a TeikhosBounty works

Normally a public key leaks the moment an address signs something. Here the address never signs. The contract stores a signature that key made over itself, XORed with the key, so the stored words look like noise. Send the right 64 bytes to authenticate() and the XOR gives back r and s. ecrecover checks them against the address the key hashes to. Wrong key, garbage signature, nothing happens.

So what do you search for? Not a private key. You never sign anything with it, and you don't need it. You need the public key, and there is no route from the contract to it: no transaction from its address, no signature, nothing on chain to recover it from. It's a 64-byte secret someone has to hand you.

Three variants

  • teikhos/0 and teikhos/1, 1 ETH each. The simple one: stored words XOR public key, ecrecover, suicide(msg.sender). Except teikhos/0 stops at the check, more on that below.
  • teikhos/2, 0.5 ETH. One more layer: the public key first unlocks a symmetric key, and that key unlocks the signature. The same code went up as a gist 48 minutes before the deploy, stored proofs included.
  • teikhos/3 and teikhos/4, 0.5 ETH each. The mask is the Keccak-512 of the key, computed by a separate SHA3_512 contract. And there's commit and reveal on top, because teikhos/1 and teikhos/2 pay whoever's transaction lands first. Anyone watching the mempool can copy a correct key and outbid you on gas.

The two commit and reveal contracts aren't twins. teikhos/4 went up two hours after teikhos/3 as a revision. In teikhos/3 you commit, authenticate() sets the solution, you call reveal() later, and if nobody reveals a valid commit in seven days, reward() sends the ETH back to the author. In teikhos/4 authenticate() reveals your commit in the same call and reverts unless it holds. No way back to the author.

How teikhos/4 fell

On April 3, 2022 an address nobody has tied to a name committed to teikhos/4 and, twenty minutes later, called authenticate() with a public key. The transaction reverted. The key was right. In teikhos/4 a correct key only reverts at the last line, require(reveal() == true), so it was their commit that didn't hold. A reverted transaction still keeps its calldata, though. The answer sat on chain for four years.

floflo777 went through the calldata of every authenticate call to the five contracts, checked each key offline and found this one. In June 2026 they committed and authenticated through Flashbots, so nobody could copy the commit, waited the seven days and called reward(). 0.5 ETH, eight years and three months after the funding.

The day they posted about it, a crowd showed up. More than a dozen addresses sent authenticate() calls to teikhos/0, teikhos/1 and teikhos/2. Nothing paid, and teikhos/0 couldn't have.

What the teikhos records carry

tssrc/collections/teikhos/4.ts
export const teikhos4 = ethereumPuzzle({
  id: "teikhos/4",
  address: standard("0x735ba26f91e1275fa4b504649b19ef74739fe7e7"),
  sourceUrl: "https://etherscan.io/address/0x735ba26f91e1275fa4b504649b19ef74739fe7e7#code",
  startedAt: "2018-03-14 03:11:51",
  status: Status.Solved,
  prize: 0.5,
  stages: [
    stage("commit", "Store a signature by your own address …", [
      artifact("verified contract source", source),
    ]),
    stage(
      "authenticate",
      "Submit the public key whose Keccak-512 unmasks the stored proof …",
      [artifact("verified contract source", source)],
      answer("ca6a98ce…c36940c6", writeup, { date: "2026-08-16" }),
    ),
    stage("reward", "After the seven days anyone can call it …", [
      artifact("verified contract source", source),
    ]),
  ],
  solvedAt: "2026-06-21 09:51:23",
  solveTime: 261_038_372,
  transactions: [funding(/* 2018 */), claim(/* 2026, reward() */)],
  solver: party("floflo777", { key: "floflo777", addresses: ["0x83e4…5156"], facts: [/* … */] }),
});

An answer that isn't a key

The winning input is a public key, and pubkey is the wrong place for it. That field means the key of the puzzle's own address, and a contract has none. So the key lives on the authenticate stage as its published answer, with floflo777's write-up as the source. The stages are the contract's own calls, one transaction each: three for teikhos/4, four for teikhos/3 with its separate reveal(), and one authenticate for the older three. The record has no key and no pubkey, and verify has nothing to check. The chain already did: reward() only pays after the right key went through authenticate().

Contracts don't sign

A claim here is a selfdestruct, not a signed spend, so a solved record has no public key to show. The validation that asks every claimed puzzle for one skips this collection, the same way it skips the Arweave weaves.

Numbered in deployment order

teikhos/4 resolves as 4, "4" or teikhos/4. The numbers are our own, the order the contracts went on chain, from 0. Etherscan calls all five TeikhosBounty, so the author's name doesn't tell them apart. Why 0 and not 1? The first contract is the odd one out, and the rest keep the numbers they'd have without it.

teikhos/0 can't pay

The first contract, 0xaec7e8c2…, went up 49 minutes before teikhos/1 and holds a bit over 1 ETH. Its authenticate() returns true on the right key and that's all. No suicide, no transfer, nothing else in the code moves ETH. So the record is there because the author funded it like the others, and it stays unsolved forever, key or no key. Its 1 ETH counts toward the open prizes, and nobody can collect it.

The TeikhosBounty records

Listteikhos.all()5 puzzles · 4 unsolved · 1 solved
  1. 0 unsolved1 ETH0xaec7e8c221c3…fd899ebf
  2. 1 unsolved1 ETH0x17e5e0910b91…10ad56a4
  3. 2 unsolved0.5 ETH0xd7c6d542f3dc…f8655805
  4. 3 unsolved0.5 ETH0x973c2178b092…af696255
  5. 4 solved0.5 ETH0x735ba26f91e1…739fe7e7
every name is a page · the key marks a known private keylocal dataset / no network