Engineyard SHA Contest Leaderboard I threw together
jazzychad.com
jazzychad.com
hashbreaker
bUGs BUgs bUGS bUgS bLAnk BlAnK BlANK BLank buGS bugS bUGS bUGs dCao2
CodingCrypto
BugS BUgs buGs BUgs bLANK Blank bLanK BlAnk BuGs BuGs BUGS bUGs FcBkw
CodingCrypto
cOWs cOwS cOwS cowS DIRtY Dirty DiRTY DIrtY cOWS Cows CoWs Cows owjo0
hashbreaker
CoWS coWS CowS cOWS DiRTY DIrtY DirTy DIRtY COWS COWS cOwS cOWS bvpDq
Edit: Also, accounts had their first tweets on the 19th. BUgs bUGS bUGs bugS BlaNK BLanK blANK bLaNK BugS BUgs BuGs bUgs liwcC
Based on his Twitter account I'm a little more inclined to believe that they are indeed separate people all using the same algorithm.Edit: This appears to be their info based on the submission by nkurz - http://news.ycombinator.com/item?id=717174
How far can you go into an SHA1 calculation and then change the input and have your calculation be useful?
For a concrete example, consider let us say you wanted to take the SHA1 of the input that is 1MB of 'a' followed by an 'a', and then 1MB of 'a' followed by a 'b', etc.
You don't have to run the SHA1 function over 'a...aa', then reset from scratch and run it over 'a...ab'. You can run it over 'a...a' without then feeding it the final byte or performing its final steps, snapshot its state (which is only some small multiple of its final output size), and then quickly calculate 'a...aa', 'a...ab', 'a...ac', etc. without redundantly feeding the identical 1MB prefix in each time.
stem_sha = hashlib.new('sha1')
stem_sha.update('ruby ruby ruby... rails rails ')
for suffix in random_char_set: # Gets random 5 char phrase
test_sha = stem_sha.copy()
test_sha.update(suffix)
...Note, though, SHA1 works in 512-bit (64-byte) blocks, so 'snapshotting' after feeding it (for example) 63 bytes hasn't saved any hard work -- the routine was only buffering those 63 waiting for byte #64 (or the digest-finish operation) to do the real crunching.
And, the tiny EngineYard strings don't offer a giant opportunity for precalculation savings. The usual pattern for digesting is:
1 x new()
N x update()
1 x digest()
The copy() lets you snapshot state after M update()s, M<N, to speed future calculations. Obviously if M is a lot -- there is a large shared prefix -- there could be big savings. But with the EngineYard strings, M should never be more than 1 block/update(). (If it is, your 10-word preamble was wastefully long.) So the potential savings are capped relative to the cost of the copy() and the (unavoidable) second update() and digest(). If the second update() and final digest() are roughly as costly as the first update() and original new(), and if the copy() is nearly costless, I suppose the speedup could approach 2x.But also: I suspect an optimized C SHA1 operation on tiny amounts of data is going to scream compared to your Python loop and calling overheads -- so any improvement from your copy() reuse may be noise compared to what's taking up most of your loop time.
I lucked out and got a solution at 35. Coded something up in python and only got about 85k hashes/sec on one core. The CUDA program, though, was about 100x faster on my low-end Nvidia 8400 GS.
I figured the 'final 5 arbitrary characters' was a nod to this approach. So the best strategy I could think of -- and I didn't try coding anything up for the contest -- was to (1) guess the word dictionary; (2) precompute a whole bunch of checkpointed-except-for-last-five-characters function states; (3) at the moment the real dictionary and target was announced, try many different last-five-character extensions against any valid precomputed states.
Why, none other than our good friend -- and my personal hero -- djb! I have no doubt that the race was actually between seibert and djb.
To the uninitiated: http://cr.yp.to
31 d241c4b4dd38aacb197c8e48e9f8488264e39a83
bUGs BUgs bUGS bUgS bLAnk BlAnK BlANK BLank buGS bugS bUGS bUGs dCao2
32 334945bffdd1000f1d198e02497188a2b17f1991
cOWs cOwS cOwS cowS DIRtY Dirty DiRTY DIrtY cOWS Cows CoWs Cows owjo0
(assuming the score are correct)I've not tried any crypto work before but this has really grabbed my interest. It just occurred to me however, that discarding lines of inquiry leading down the "BuGS bugs bUgs BUGs BLAnK BlAnK....." route in favour of something that made more sense grammatically could be a good tactic to save time?.... or it could just increase the overhead in generating the next hash?...
Say for instance you coded four stages:
1/ Generating a list of strings for analysis 2/ Parsing that list for sentences that made sense 3/ Hashing these sentences 4/ Comparing the hashes generated with the one being sought
to me it just looked as though a lot of gibberish was analysed because there is a lot more entropy than order to sift through and nobody tried step 2... does my logic make sense or am I getting mixed up and not really seeing the bigger picture?
So unless you want to find the exact sequence, it does not matter whether you candidate strings make sense or not. And since the search space is so large, people just gave up on finding the exact sequence. They just use a lot of cycles to find any sequence, that is close in hash.
A sentence that starts 'Be' (001000010...) and the same sentence changed to 'He' (001001000...) would produce very different hash results by the Avalanche Effect ( http://en.wikipedia.org/wiki/Avalanche_effect ). So having similar letters in a similar order or in similar positions, or using similar length words, wont help.
Isn't that the whole point of this competition?
The point was to get a hash with a similar quantity of 0 and 1 bits in the output as their hash, not to guess their sentence (they said what it was).
Ahh, I missed that! That makes sense in terms of the Avalanche Effect - cheers for the knowledge
ETA: I also find it interesting that the lowest score from those not using the optional 5 random chars is currently 38. All the lower ones use random chars.