TeikhosBounty is a family of on-chain cryptographic bounties by Johan Nygren (@resilience-me,
author of EIP-935). Each contract holds some ETH and pays it to whoever submits the 64-byte
public key of a specific account, where a signature (r, s) is hidden in the contract storage
by XOR. This writeup covers one of them, 735B
(0x735ba26f91e1275fa4b504649b19ef74739fe7e7),
which held 0.5 ETH. I solved and claimed it.
The other bounties in the family are still open, so this repo deliberately documents only 735B and omits any data that would help claim the rest.
The contract knows an account by its address, which is keccak256(pubKey)[-20:]. It wants the
full 64-byte public key back. Because an address is a one-way hash of the key, you cannot read
the key off-chain from the address alone. Normally you would recover a public key from any
transaction the account has signed, since an ECDSA signature leaks the signer's key. But the
735B target
(0x6a9d1afecb3ad5c8964752f0f893ebb3dfe13f92)
is a ghost account: nonce 0, never funded, never signed anything on mainnet. So there is no
signature to recover the key from, and brute forcing a 256-bit key space is out of the
question. On its face the key is simply not available anywhere.
If the account never signed anything, the only place its public key could appear is in someone else's failed attempt to claim the same bounty. So instead of attacking the cryptography, I harvested the full history of calldata sent to the TeikhosBounty family and looked for 64-byte public keys that had been submitted and had not resulted in a payout.
One did. A 2022 transaction (block 14513600) had submitted the correct public key to 735B, but the attempt failed to claim: the bounty uses a keccak512 variant and a commit-reveal payout, and that attempt did not complete it correctly. The winning key had been sitting in plain mainnet calldata ever since, unnoticed because the transaction had reverted and looked like just another dead attempt.
winning public key (64 bytes):
ca6a98ceec61e213d9a0a8fdc0a6d5d9ed7566f5f4cfd24871fb9316feb6e1eb
2367489f54a0cd4111f4c5356eb744d299a7521296786223c70947c8c36940c6
Before touching mainnet I confirmed the key was correct. For the keccak512 variant, the hidden
signature (r, s) is reconstructed from the XOR'd proof words held in the contract. With the
candidate public key and the reconstructed signature, the check is self-contained: the key's
keccak-derived address has to match the bounty's target, and the signature has to verify
against that key. It did, exactly. A useful property here is that you only ever need the 32-byte
X coordinate of a public key; the Y coordinate is reconstructed from the secp256k1 curve
equation, so a single leaked coordinate is enough to rebuild and test a full key.
735B pays through a commit-reveal with a time delay, which exists precisely to stop someone
front-running your reveal in the mempool. I committed, waited out the delay, then revealed and
called reward(), and the contract paid out its 0.5 ETH by selfdestructing to the winner
address.
The hard part of this puzzle was not cryptography, it was realising the cryptography did not need to be broken. A bounty that has been public for years accumulates failed attempts, and a failed attempt can leak exactly the secret the bounty is paying for. The move was to treat the chain's own history as the search space instead of the key space.
Solved and claimed by @0xFlorent_, ENS 0xflorent.eth.