Workspace IndexMath › Day 6

Pigeonhole Principle to Hash Collisions TODO

Math · Day 6 / 52 · July — Discrete Math & Logic (Day 3-10)

Concept

The pigeonhole principle states that if you put more than n items into n boxes, at least one box gets two or more. Generalized, putting m items into n boxes means some box gets at least ⌈m/n⌉. Since a hash function maps an effectively infinite domain to a fixed-length output, this principle proves that collisions must exist — there's no avoiding them. What a cryptographic hash actually aims for isn't the absence of collisions but that finding one is computationally hard. Layer the birthday problem on top of this, and for a b-bit output, a random collision is expected within roughly 2^(b/2) attempts, so collision resistance is effectively about half the output length.

Mistaking output bit-length for security strength — say, by truncating a hash or using a short identifier — quietly halves your strength against collision attacks (as opposed to preimage attacks). You need to know which resistance property actually matters for the job to pick a safe length.

Code & Formula

# 비둘기집 원리 → 해시 충돌 — 상자(버킷)보다 물건(입력)이 많으면 충돌은 "반드시" 생긴다.
# 8비트로 자른 해시(256개 버킷)에 무작위 입력을 계속 넣어 첫 충돌이 나오는 시점을 관찰.

import hashlib

def short_hash(data: bytes, bits: int) -> int:
    full = hashlib.sha256(data).digest()
    value = int.from_bytes(full, "big")
    return value % (2 ** bits)   # bits비트로 잘라 버킷 인덱스로 사용

def find_first_collision(bits: int, seed: int = 0):
    buckets = {}
    i = seed
    while True:
        item = f"item-{i}".encode()
        idx = short_hash(item, bits)
        if idx in buckets:
            return i - seed + 1, buckets[idx], item   # 시도 횟수, 먼저 있던 입력, 충돌 입력
        buckets[idx] = item
        i += 1

BITS = 8   # 256개 버킷뿐이라 비둘기집 원리상 257번째 입력까지 가면 충돌이 강제됨
n_buckets = 2 ** BITS
tries, first, second = find_first_collision(BITS)

print(f"버킷 수 = 2^{BITS} = {n_buckets}")
print(f"첫 충돌까지 시도 횟수: {tries}  (비둘기집 원리상 최대 {n_buckets + 1}회 이내 보장)")
print(f"  충돌한 두 입력: {first!r} , {second!r}")

# 생일 문제 근사: 무작위 충돌은 대략 2^(bits/2) 시도에서 기대된다.
expected = int(2 ** (BITS / 2))
print(f"생일 문제 근사 기대 시도 수 ≈ 2^({BITS}/2) = {expected}")

Exercise

Truncate keccak256 output to 32, 48, and 64 bits, measure how many random-input attempts it takes to hit the first collision at each length, and compare against the 2^(b/2) prediction.

Practical Connection

Verex's Conditional Tokens derive conditionId, collectionId, and positionId all as hashes, so truncating these identifiers for use as indexing keys creates a real collision risk, and the same math applies to Merkle-tree-based proofs.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/math-50-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

비둘기집 원리 TODO

Math · Day 6 / 52 · 7월 — 이산수학·논리 (Day 3–10)

개념

비둘기집 원리는 n개의 상자에 n보다 많은 물건을 넣으면 적어도 한 상자에는 두 개 이상이 들어간다는 진술이다. 일반화하면 m개 물건을 n개 상자에 넣을 때 어떤 상자에는 m/n의 올림 이상이 들어간다. 해시 함수는 사실상 무한한 정의역을 고정 길이 출력으로 보내므로, 이 원리에 의해 충돌의 존재 자체는 증명적으로 피할 수 없다. 암호학적 해시가 노리는 것은 충돌이 없다는 것이 아니라 충돌을 계산적으로 찾기 어렵다는 성질이다. 여기에 생일 문제가 겹쳐, b비트 출력에서 무작위 충돌은 약 2의 b/2제곱 번의 시도에서 기대되므로 충돌 저항 강도는 출력 길이의 절반 수준이다.

출력 비트 수를 그대로 보안 강도로 착각해 해시를 잘라 쓰거나 짧은 식별자를 쓰면, 원상 공격이 아니라 충돌 공격 쪽에서 강도가 절반으로 떨어진다. 어떤 저항성이 필요한 자리인지 구분해야 안전한 길이를 고를 수 있다.

코드 · 수식

# 비둘기집 원리 → 해시 충돌 — 상자(버킷)보다 물건(입력)이 많으면 충돌은 "반드시" 생긴다.
# 8비트로 자른 해시(256개 버킷)에 무작위 입력을 계속 넣어 첫 충돌이 나오는 시점을 관찰.

import hashlib

def short_hash(data: bytes, bits: int) -> int:
    full = hashlib.sha256(data).digest()
    value = int.from_bytes(full, "big")
    return value % (2 ** bits)   # bits비트로 잘라 버킷 인덱스로 사용

def find_first_collision(bits: int, seed: int = 0):
    buckets = {}
    i = seed
    while True:
        item = f"item-{i}".encode()
        idx = short_hash(item, bits)
        if idx in buckets:
            return i - seed + 1, buckets[idx], item   # 시도 횟수, 먼저 있던 입력, 충돌 입력
        buckets[idx] = item
        i += 1

BITS = 8   # 256개 버킷뿐이라 비둘기집 원리상 257번째 입력까지 가면 충돌이 강제됨
n_buckets = 2 ** BITS
tries, first, second = find_first_collision(BITS)

print(f"버킷 수 = 2^{BITS} = {n_buckets}")
print(f"첫 충돌까지 시도 횟수: {tries}  (비둘기집 원리상 최대 {n_buckets + 1}회 이내 보장)")
print(f"  충돌한 두 입력: {first!r} , {second!r}")

# 생일 문제 근사: 무작위 충돌은 대략 2^(bits/2) 시도에서 기대된다.
expected = int(2 ** (BITS / 2))
print(f"생일 문제 근사 기대 시도 수 ≈ 2^({BITS}/2) = {expected}")

연습

keccak256 출력을 앞에서 32비트, 48비트, 64비트로 잘라 각각 무작위 입력으로 첫 충돌이 나올 때까지 걸린 시도 횟수를 측정하고 2의 b/2제곱 예측과 비교하라.

실무 · Verex 연결

Verex가 쓰는 Conditional Tokens는 conditionId·collectionId·positionId를 모두 해시로 유도하므로 이 식별자를 짧게 잘라 인덱싱 키로 쓰면 충돌 위험이 실제로 생기고, Merkle 트리 기반 증명에서도 같은 계산이 적용된다.

공부한 날 원본 커리큘럼(docs/knowledge/math-50-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 5. 그래프 기초(DAG·트리·해시 링크)7. Big-O & 가스 →