Break the Hash Table
Notes 06 ended with a warning: if anyone can predict where a hash function sends keys, a troublemaker can aim thousands of keys at the same bucket and turn the instant coat check into one long, sad line. This week, you are the troublemaker. I'm handing you a hash table with a terrible ticket system — the "add up the letters" hash from class — and your mission is to find five real English words that all crash into the same bucket, on purpose.
That's it. No report, no analysis, no diagrams — one provided script, five well-chosen words, one screenshot of the wreckage. And because every heist needs style, you may name your villain operation in a comment at the top. Operation Bucket List. The Collision Collective. Zero points, strongly encouraged.
The whole projectThree steps
Case the joint
Copy the script below into break_the_hash.py and run it with any Python 3. It builds two coat checks with 256 hooks each — one on the bad "add up the letters" hash, one on Python's built-in hash — loads about 150 common words into both, times roughly 125,000 lookups against each, and prints each table's longest rack (the biggest pile of words on one hook). Notice the bad table is already slower — and notice the free clue at the bottom of the output.
Hunt your five words
Find five different real words that the bad hash sends to the same bucket. The script includes a hunting tool: run python -i break_the_hash.py, then type where("yourword") to see which bucket any word lands in. Guess and check is completely legal — it's what a real attacker would do. Figure out why the free-clue words collide, and the hunt gets very short.
Commit the crime
Put your five words into the marked YOUR_FIVE list and run the script once more. It verifies the attack and prints VILLAIN CHECK PASSED plus how crowded your bucket got. Take one screenshot of that final output — your five words sharing a bucket, the bad table's long rack next to the good table's short one, and the timing gap. That picture is your entire evidence.
Provided codeThe whole script
Everything is here — the tiny hash table (chaining, straight from Notes 06), both hash functions, the word list, the stopwatch, and the villain check. The only thing missing is your five words.
# break_the_hash.py — AI 230 · Project 2 · Break the Hash Table # One tiny coat check, two ticket systems: a good one and a terrible one. # Mission: find FIVE real words that all pile onto the SAME hook under # the bad hash. Put them in YOUR_FIVE below and run me again. import time BUCKETS = 256 # a fixed little coat check: 256 hooks, numbered 0-255 def bad_hash(word): # "Add up the letters" from Notes 06: a=1, b=2, ... z=26, then sum. # Same word always gives the same number — job 1, done perfectly. # But adding doesn't care about ORDER. Remember that. return sum(ord(c) - 96 for c in word.lower() if c.isalpha()) def good_hash(word): # Python's built-in scrambler — the professionals' ticket system. return hash(word) class TinyTable: # chaining, exactly like Notes 06 Section 4: one small rack per hook def __init__(self, hash_fn): self.hash_fn = hash_fn self.racks = [[] for _ in range(BUCKETS)] def bucket_of(self, word): return self.hash_fn(word) % BUCKETS # wrap onto a hook number def add(self, word): rack = self.racks[self.bucket_of(word)] if word not in rack: rack.append(word) def has(self, word): # the lookup we time below return word in self.racks[self.bucket_of(word)] def longest_rack(self): return max(len(r) for r in self.racks) def where(word): # your hunting tool: which hook does the BAD hash pick? print(f" bad hash sends {word!r} to bucket {bad_hash(word) % BUCKETS}") # ====================================================================== # YOUR FIVE VILLAIN WORDS GO HERE — five DIFFERENT real words that all # land in the SAME bucket under the bad hash. Hunt interactively with: # python -i break_the_hash.py then where("yourword") YOUR_FIVE = [] # ====================================================================== WORDS = """ time year people day man thing woman life child world school state family student group country problem hand part place week company system program question work night point home water room mother area money story month study book job word business side kind head house service friend father power hour game line member law car city name team minute idea body parent face level office door health person art history party result change morning reason girl moment teacher force education policy music market sense nation plan college interest effect class control care field role rate heart leader light voice police mind price report flower spoon window garden winter summer bridge castle ocean paper town road wind peace trade star bird song iron rain snow stone tree cake corn farm gate lake pond rope sand wool clay moss pine reed silk empty fresh grand heavy plain round sharp bake call fall grow hunt jump """.split() if __name__ == "__main__": bad, good = TinyTable(bad_hash), TinyTable(good_hash) everything = WORDS + [w for w in YOUR_FIVE if w not in WORDS] for w in everything: bad.add(w) good.add(w) def clock(table): # time 800 lookups of every single word start = time.perf_counter() for _ in range(800): for w in everything: table.has(w) return time.perf_counter() - start print(f"stored {len(everything)} words in two {BUCKETS}-bucket tables\n") for name, t in (("BAD hash (add the letters)", bad), ("GOOD hash (python built-in)", good)): print(f" {name}: longest rack {t.longest_rack():3d}" f" {len(everything)*800:,} lookups in {clock(t):6.3f} s") if len(set(YOUR_FIVE)) == 5: spots = sorted({bad_hash(w) % BUCKETS for w in YOUR_FIVE}) if len(spots) == 1: print(f"\n VILLAIN CHECK PASSED: {', '.join(YOUR_FIVE)}") print(f" all five crammed onto hook {spots[0]} -- its rack " f"now holds {len(bad.racks[spots[0]])} words") else: print(f"\n not yet: your words spread over buckets {spots} -- " f"they must all share ONE bucket") else: print("\n no villain words yet -- put five different words in YOUR_FIVE.") print(" hunt with: python -i break_the_hash.py then where(\"word\")") print(" a free clue:") where("cat") where("act")
Your final output is six lines or so: something like BAD hash: longest rack 9 ... 0.094 s against GOOD hash: longest rack 4 ... 0.022 s, then VILLAIN CHECK PASSED naming your five words and the hook they jammed. That gap is Notes 06 happening live: every lookup in a crowded bucket walks the whole rack — O(1) collapsing to O(n), on your own machine, because you pushed it.
This is why Notes 06 kept nagging about spreading keys out. Around 2011, researchers predicted web frameworks' hashes and flooded them with colliding keys — real servers crawled to a stop, and that's why Python now stirs a secret random ingredient into its string hashes. You are about to execute a tiny, legal, 150-word version of a real denial-of-service attack. Put it on your resume with care.
SubmissionWhat you hand in
- Your script —
break_the_hash.pywith your five words inYOUR_FIVE(and your operation's name, if it has one). Attach the file or link a GitHub repo, either is fine. - One screenshot of the final run: the two comparison lines plus the
VILLAIN CHECK PASSEDlines.
No report. No write-up. No diagram. No cost table. The screenshot is the evidence — real villains don't file paperwork. Late submissions lose 10% per calendar day, per the course policy.
GradingFive checkboxes, 20 points each
Each box is pass/fail. There is no partial credit inside a box and no rubric beyond this table.
| Checkbox | Points | |
|---|---|---|
| ☐ | The script runs clean. python break_the_hash.py executes without errors and prints the two-table comparison. | 20 |
| ☐ | Your five words are real villains. Five different real words in YOUR_FIVE, and the script prints VILLAIN CHECK PASSED — they genuinely share one bucket under the bad hash. | 20 |
| ☐ | The damage shows in the racks. The output shows the bad table's longest rack at 5 or more (your five guarantee it) while the good table's stays short. | 20 |
| ☐ | The screenshot shows the whole crime. One picture with the timing comparison — bad table visibly slower — and the villain check lines, plausibly produced by the submitted script. | 20 |
| ☐ | On time on Brightspace. Script and screenshot in by Monday, November 9, 2026, 11:59 PM. | 20 |
| Total | 100 |
FAQSix questions, six answers
Where on earth do I find five words that collide?
Look hard at the free clue: cat and act land in the same bucket. Why would that be, for a hash that adds the letters? Words made of the same letters are your friends — and English has whole families of them. Beyond that, where("word") plus guess-and-check is completely legal; the hunt usually takes five minutes once the clue clicks.
My five words collide in a different bucket than my friend's. Is that wrong?
Not at all. Any five different real words sharing any one bucket passes. There are many valid gangs — the bucket number itself doesn't matter.
Can I write my own bad hash instead of the letter-sum one?
Sure — swap the body of bad_hash. Two rules: it must still be a real hash function (same word, same bucket, every time — job 1 from Notes 06), and your five words must still collide under it, proof line printed. The letter-sum hash is already a perfectly good victim, though.
The GOOD table's numbers change every run. Is my script broken?
No — that's Python defending itself against exactly this project. Since the 2011 attacks, Python mixes a secret random ingredient into string hashes on every run, so the good table's racks shuffle around. Your attack is untouched: the letter-sum hash has no secret ingredient, so your five words hit the same bucket every single run. Any Python 3 works.
Why does this matter outside of class?
Because a predictable hash turns O(1) lookups into O(n) scans, and attackers know it. The 2011 hash-flooding advisories showed one well-aimed flood of colliding keys slowing major web frameworks to a crawl — the same trick you just pulled, at scale. Every "spread the keys out" rule in Notes 06 exists because of this failure mode.
Do I have to write anything at all?
No. No sentence, no README, no report. Five words in a list, one screenshot. The only writing is optional: your villain operation's name, in a comment, for zero points and eternal glory.