read.cash Log in
@dankwing.duck more from that month

Puzzhole Solution Explained When I first wrote Puzzhole I was confident that it would get solved, but I had no idea how long until it would get solved, especially considering I wasn't able to fund it with a large prize pool. Immediately after it went live I noticed that some users began to say that it might not be solvable and I was beginning to get worried. https://read.cash/@dankwing.duck/puzzhole-wallet-solved-a69ec3bd But then Reddit user -johoe posted that he had solved it at the same time that *half* of the funds of the wallet were removed! Congratulations! You guys probably already know him from his well known tool: Johoe's Bitcoin Mempool Size Statistics. https://www.reddit.com/user/-johoe/ https://jochen-hoenicke.de/queue/#BCH,24h,weight Also nice of him to leave half the prize pool for future solvers. Classy! 🍷(Upon request he later swept the remaining before I published this article) The Solution Firstly, here is the original puzzle image again:

Discovering the Symbols As you can see the image is composed of many different symbols. If you break them down you'll find there are 8 symbols in total:

Each symbol is about how many segments (or "holes") there are. A zero is such because it has no segments or holes in it, a one is such because it has one hole in the triangle, two because it has two holes in the figure eight and so on. When I asked Johoe about how he discovered this and the process of figuring it out he said: Counting gave me 8 different symbols. I first tried to count strokes but didn't work out (not even with counting loops as two strokes; no 7 strokes), then counting the regions worked out. Only after I solved it, I noticed the puzzhole title. Once you understand that system you can take all of the symbols and put them into some kind of variable: glyphs = "45567212625722..." Turning Symbols into Bits Next you need to turn those various numbers into bits. Since there are 8 symbols, that's 3-bits per symbol since *log2(8) == 3*: for n in glyphs: word = bin(int(n))[2:].rjust(3, '0') bits += word Now you have a large binary string: 10010110111011101000101011001010111... Turning Bits into ASCII private key Lastly you can turn all of those bits into a private key: key_bytes = [] for n in range(0, len(bits), 8): word = int(bits[n:n+7], 2) key_bytes.append(word) print(bytearray(key_bytes).decode('ascii', 'ignore')) This will leave you with the private key in WIF format: KwEezDy4Hjr... And you're done! I asked Johoe to share his script he used to solve the puzzle and he graciously did. It's written in Perl and a bit of a brain twister, but here it is: @lines = qw( 45567212 ... 454 ); my @bin = ('000','001','010','011', '100','101','110','111'); while(<>) { s/[0-7]/$bin[$&]/g; $j = 0; for $i (0..24) { $_ =~ /^([01])(.*)$/; $bit = int($1); $_ = $2; $j |= ($bit << (7-($i & 7))); if ($i % 8 == 7) { print chr($j/2); $j=0; } } } His code works around some oddities that were created as a byproduct of how I encoded all the bits in Python. Sorry for that! Questions for Johoe I asked Johoe why he gave the puzzle a try whereas it seemed some other people got caught up on trust issues and labeled it a scam, he said: I like puzzles, so I thought I try a few minutes to see if I can make progress. And that worked out well here. When asked about why he only took half of the prize pool: I wanted to give others a chance to try to solve this puzzle. I didn't do it for the money in the first place. Did the rotation have anything to do with the solution? No, it was a red herring. It simply looked better with a random rotation applied to each symbol. Here is what the puzzle looks like without the rotation:

Where are the scripts that were originally used to generate the puzzle? Here is the original script that generates the puzzle image from a private key: from math import sqrt, ceil from PIL import Image from random import randrange import sys private_key = 'REDACTED WIF key' key_bytes = private_key.encode('ascii') bits = bin(int.from_bytes(key_bytes, byteorder='big')) if bits[0:2] != "0b": print("Expected 0b at start of bit string") sys.exit(1) bits = bits[2:] print("bits", bits) total_bytes = len(key_bytes) total_bits = len(bits) total_glyphs = ceil(total_bits / 3) grid_size = ceil(sqrt(total_glyphs)) print("key is", total_bytes, "bytes") print("key is", total_bits, "bits") print("key has", total_glyphs, "glyphs") print("grid size is", grid_size) # Pad bits bits = bits + "000" col = 0 row = 0 width = grid_size * 100 height = grid_size * 100 img = Image.new(mode="RGBA", size = (width, height), color='#fff') glyphs = {} glyph_encoding = "" for i in range(0, 8): glyphs[i] = Image.open("puzzhole_images/" + str(i) + ".png") print("bits", bits) col = 0 row = 0 for i in range(0, total_bits, 3): n = int(bits[i + 2]) | (int(bits[i + 1]) << 1) | (int(bits[i]) << 2) glyph_encoding += str(n) g = glyphs[n] g = g.rotate(randrange(0, 360)) img.paste(g, (col * 100, row * 100)) col += 1 if col >= grid_size: col = 0 row += 1 print("glphs", glyph_encoding) img.save('puzzhole_tmp.png') # img.show() And here is the test solver Python script: glyphs = "4556..." bits = "" for n in glyphs: word = bin(int(n))[2:].rjust(3, '0') bits += word bits += "" print("bits", bits) # If testing bits just paste them here and uncomment #bits = "10010..." key_bytes = [] for n in range(0, len(bits), 8): word = int(bits[n:n+7], 2) key_bytes.append(word) print(bytearray(key_bytes).decode('ascii', 'ignore'))

No comments yet

Log in to join in Reading is open to everyone. Replying needs an account.