Collections

kTimesG's 80-bit key challenge

kTimesG hid 80 unknown bits in a 511-bit key and showed its public key only in an unconfirmed spend. Someone cracked it and swapped the spend 39 minutes later.

October 16, 2024. kTimesG announced a side challenge in the Bitcointalk thread of the b1000 puzzle. The deal: on November 1 at midnight UTC, a P2PKH address with 0.005 BTC and "an 80-bits secure private key". Plus the range to search. The winner only had to say which method broke it. And there was a line for everyone about to argue: "If your first instinct is to yell 'this is not a 80-bits secure private key', please note this competition is NOT for you."

ID80-bit01 / 281 puzzle · 2024-11-01
Collection

kTimesG's 80-bit key challenge

bitcoin

80 unknown bits in a 511-bit key. The public key only ever showed up in the mempool. Taken 39 minutes in.

Author
kTimesG
Prize recorded
0.005 BTC
Still unsolved
none
closed 1 / 1

Census

solved
1100%
public keys
1100%
private keys
1100%

Access

LoadgetCollection("80-bit")
Show80-bit in the playground

How the 80-bit challenge works

80 bits in the middle of a 511-bit number

The range post went up at 00:01:53 UTC. It's not a range from 1 to 2^80:

text
minKey = 0x659756abf6c17ca70e0000000000000000000140be6ddd93e441f8d4b4a85653b20b4cdcc5c748207a0daa16191d07a425d8080c276f9412472e0429e61bc355
maxKey = 0x659756abf6c17ca70fffffffffffffffffffff40be6ddd93e441f8d4b4a85653b20b4cdcc5c748207a0daa16191d07a425d8080c276f9412472e0429e61bc355

Both numbers are 511 bits long. They differ only in 80 contiguous bits, starting at bit 361. A number that big still works as a private key. It just wraps around the curve order n.

The trick is to move the unknown bits to the bottom. Subtract minKey·G from the public key. Multiply by the inverse of 2^361 mod n. What's left is an ordinary key between 1 and 2^80, and Kangaroo eats that in minutes. The answer, 0x2d56cbf370cbeef9e80a, goes back the same way. minKey | (k << 361), then mod n, gives b40e7d34…6be5fe56.

So yes, 80-bit secure. Some people in the thread still spent the day arguing it wasn't.

The public key only lived in the mempool

The address got two payments of 0.0025 BTC on October 28 and never spent. No spend, no public key, nothing to search with. The author promised to push a spend 20 to 28 hours after the range post. And the hint said where the hard part was: "The challenge involves correctly extracting pubkey from the raw TX, otherwise it's a no brainer."

So the real game was a race. Pull the public key out of the pending spend. Crack it. Replace the author's transaction with a higher fee, all before a block confirms it.

The author pushed right after a block, and the next one took 57 minutes. Plenty of time. albert0bsd posted the transformed public key minutes after the spend showed up. Still, nobody replaced it for 39 minutes. kTimesG wasn't impressed: "What if this was the real 80 bit puzzle being emptied, you'll never have 40 minutes." Etar's script had the key in 158 seconds and then crashed on the fee. Ouch.

The replacement landed in block 868480 at 00:37:21 UTC on November 2. 0.00495 BTC went to 14q4SoQwENXXzsVT3GMwDrDUGiW5QZeiDg. The author's own spend never confirmed, so the chain shows only the replacement.

What the kTimesG record carries

tssrc/collections/80-bit.ts
export const kTimesG80Bit = bitcoinPuzzle({
  id: "80-bit",
  address: p2pkh("1ECDLP8osCZHBB1LH5PVAUfFegeMgFb52q", "90b881be…8ba92a12"),
  sourceUrl: RANGE,
  startedAt: "2024-11-01 00:01:53",
  status: Status.Solved,
  pubkey: compressed("03a61fc84b6429f07fc0edf25265ef7a0ced3cd9a0edea85e9f58b50b5d73f66e7"),
  key: hex("b40e7d34265ab9533a64622bd1a188fb8abb8829af545169abad49b46be5fe56"),
  prize: 0.005,
  hints: [/* the 80 contiguous bits, the range with its answer, the raw TX */],
  solvedAt: "2024-11-02 00:37:21",
  solveTime: 88528,
  transactions: [funding(/* … */), increase(/* … */), claim(/* … */)],
  solver: party(undefined, { addresses: ["14q4SoQwENXXzsVT3GMwDrDUGiW5QZeiDg"] }),
});

A 256-bit key and no bits

The key is the reduced one, the 256-bit number that actually signs for the address. It has no bits, on purpose. bits has to match the key's own length, and 80 is the width of the range, not the key. The range and the 511-bit form sit in the hints, where the author put them.

The range hint also carries the answer. Before anyone had the public key, the range post committed to the solve steps with a SHA-256, a6c39217…f0651db0. The steps came out after the claim. The record quotes their last lines: shift 361, the 80-bit key and the % that turns it into b40e7d34….

Started at the range, not at the funding

startedAt is the range post, not the funding four days earlier. The author did post the address on October 31, to show it wasn't a hoax. But without the range there was nothing to search. And without the pending spend, nothing to search with. So solveTime is 88,528 seconds, about 24.6 hours, and almost all of it was waiting for the spend.

A solver with an address and no name

opchecksig wrote later that the prize address is theirs. Kangaroo on an RTX 4070, 28 minutes. Nobody signed anything with that address though. So the record keeps the address and skips the name.

A singleton id, no name segment

80-bit is the whole identifier, like genesis, gsmg and the other singletons. It used to be ktimesg/80-bit, the author's handle as the collection key for a collection that only ever held one challenge. kTimesG keeps the author page. The old pages redirect here.

The kTimesG challenge record

ID80-bit2024-11-01 → 2024-11-02
Puzzle / 80-bit

bitcoinp2pkh solved

Record

bitcoinPuzzle({
  address: p2pkh("1ECDLP8osCZH…eMgFb52q", "90b881be7044…8ba92a12"),
  status: Status.Solved,
  key: hex("b40e7d34265a…6be5fe56"),
})
Prize
0.005 BTC
Key material
hex
Verification
verified
Solved
2024-11-02 · 1d 35m
tx 3
Trail
Explorerblockstream.info
Sourcebitcointalk.org/index.php?topic=1306983.msg64691846
Try80-bit in the playground
Address
1ECDLP8osCZHBB1LH5PVAUfFegeMgFb52q

hash160 90b881be7044a596b0ac843d84ff31278ba92a12

Balance
asking the explorer

Key

public key · compressed
03a61fc84b6429f07fc0edf25265ef7a0ced3cd9a0edea85e9f58b50b5d73f66e7
private key (hex)
b40e7d34265ab9533a64622bd1a188fb8abb8829af545169abad49b46be5fe56

Transactions

  1. funding2024-10-280.0025 BTCe4a2751bf7…43568b
  2. increase2024-10-280.0025 BTCa7d8b7f7a8…c5b2f9
  3. claim2024-11-020.00495 BTCdc8309f8b5…162f36

Hints

  1. 2024-10-16

    officialThe Hamming length of the range will therefore be 80 contiguous bits, but they may start anywhere.

  2. 2024-11-01

    officialminKey = 0x659756abf6c17ca70e0000000000000000000140be6ddd93e441f8d4b4a85653b20b4cdcc5c748207a0daa16191d07a425d8080c276f9412472e0429e61bc355 maxKey = 0x659756abf6c17ca70fffffffffffffffffffff40be6ddd93e441f8d4b4a85653b20b4cdcc5c748207a0daa16191d07a425d8080c276f9412472e0429e61bc355

    Published answer

    assert shift == 361; elem = shift_inv * (public_key - min_elem); assert idlp_key == 0x2d56cbf370cbeef9e80a; private_key = min_key | (idlp_key << shift); assert private_key % secp256k1.N == 0xb40e7d34265ab9533a64622bd1a188fb8abb8829af545169abad49b46be5fe56

    2024-11-02bitcointalk.org/index.php?topic=1306983.msg64695204

  3. 2024-11-01

    officialThe challenge involves correctly extracting pubkey from the raw TX, otherwise it's a no brainer.

local record / balance from the explorer