시험 벡터로 처음부터 끝까지 따라 하는 SNOW 휠 공정성 검산
SNOW의 공정성 검증 페이지는 휠 추첨 계산식과 원본 코드(draw.go)를 공개하고, 계산기에 ‘시험 예시’를 넣어 볼 수 있게 해 둡니다. 이 시트는 그 시험 예시 하나를 손으로 다시 계산합니다. 여기서 같은 값이 나오면, 실제 영수증의 검증 자료도 같은 방법으로 직접 확인할 수 있습니다.
- 84,923
- 추첨값 (0~9,999,999)
- 0번 칸
- 누적 가중치 100,000
- 0회
- 다시 뽑은 횟수
- 8.3×10⁻⁷¹
- 다시 뽑을 확률
검산으로 확인되는 것과 확인되지 않는 것
공정성 검증은 ‘기록된 결과가 당시 입력값으로 계산한 결과와 같은지’를 확인하는 방법입니다. ‘당첨이 잘 나오는지’를 알려 주는 도구는 아닙니다. SNOW 페이지도 계산이 같아도 공개 시점, 장부 연결, 재고, 실제 지급까지 증명하지는 않는다고 적고 있습니다.
| 질문 | 검산으로 답할 수 있나 | 필요한 것 |
|---|---|---|
| 서버 시드가 추첨 뒤에 바뀌지 않았나 | 예 | 추첨 전에 받아 둔 server_seed_hash |
| 보상표 가중치가 바뀌지 않았나 | 예 | 추첨 전에 받아 둔 table_version_hash |
| 내 시드·커밋으로 같은 칸이 나오나 | 예 | 영수증의 calculation_proof |
| 해시가 실제로 추첨 전에 공개됐나 | 아니요 | 별도 기록(내가 받아 둔 사본) |
| 보상이 실제로 지급됐나 | 아니요 | 지급 내역 |
| 슬롯·라이브 카지노 결과 | 아니요 | 게임사 별도 시스템 |
그래서 검산을 제대로 하려면 추첨 전에 시드 해시와 보상표 해시를 따로 저장해 두어야 합니다. 추첨 뒤에 한꺼번에 받은 자료만으로는 ‘계산이 맞다’까지만 확인됩니다.
필요한 값과 형식 규칙
| 항목 | 형식 | 규칙 |
|---|---|---|
| server_seed | 64자리 16진수 = 32바이트 | 추첨 뒤 공개. 해시·HMAC에는 글자가 아닌 32바이트를 씀 |
| server_seed_hash | 64자리 16진수 | SHA-256(32바이트 시드). 추첨 전 공개 |
| client_seed | UTF-8 문자열 | NFC 정규화 뒤 1~128바이트. 제어 문자, |, @ 불가. 공백은 그대로 |
| brand_env | 문자열 | 비어 있으면 안 되고 |와 제어 문자 불가 |
| commit_id | 소문자 UUID | 추첨마다 다름 |
| canonical_document | JSON 원문 | 받은 글자 그대로 해시. 칸 위치·가중치·기본 칸 표시 |
계산기 화면의 ‘대조할 브랜드 환경’ 칸이 brand_env입니다. 시험 예시의 brand_env는 test이고 실제 영수증은 영수증에 적힌 값을 씁니다.
시험 벡터를 한 단계씩 계산하기
계산기의 ‘시험 예시 넣기’에 들어 있는 값입니다. 서버 시드는 0을 62개 쓰고 끝에 62를 붙인 64자리 16진수이고 보상표는 0번 칸 100,000, 1번 칸 50,000, 2번 칸(기본 칸) 9,850,000 세 칸입니다. 당첨 확률로 바꾸면 1%, 0.5%, 98.5%입니다.
서버 시드 해시
64자리 글자를 32바이트로 풀어 SHA-256을 구합니다.
0010d2c57c2552b023f787cafcd6cd962d6faa04a7621fba14faa3a9f6de8910
보상표 해시
canonical_document 원문을 UTF-8 바이트 그대로 SHA-256에 넣습니다.
3e26d57e91ece2366848e3d9f4dc2eaf6288b24e405b12314a87555787b90110
메시지
알고리즘 번호 1, brand_env, WHEEL, NFC로 정규화한 client_seed, commit_id를 |로 잇습니다.
1|test|WHEEL|c98|aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
HMAC-SHA256
키는 32바이트 서버 시드, 내용은 메시지의 UTF-8 바이트입니다.
8a329eff3157fa17ca047ba0014fbe4c5e1fc27e2c4387e61b47d48b2ed4383b
256비트 정수
32바이트를 앞자리부터 읽어 부호 없는 정수 H로 바꿉니다. H가 경계값 L보다 작으므로 첫 시도(접미사 없음)에서 통과합니다.
H = 62508612813635367744208359319450530045745479443396350918367385282301090084923
추첨값
H를 10,000,000으로 나눈 나머지입니다. H의 끝 일곱 자리가 그대로 추첨값이 됩니다.
value = 84923
칸 선택
위치 순서대로 가중치를 더하다가 누적값이 추첨값보다 처음 커지는 칸을 고릅니다. 0번 칸에서 누적 100,000이 84,923보다 크므로 0번 칸입니다.
쏠림을 없애는 경계값 L
2256은 10,000,000으로 나누어떨어지지 않습니다. 그냥 나머지를 쓰면 0부터 시작하는 일부 추첨값이 아주 조금 더 자주 나옵니다. 그래서 10,000,000의 배수 중 2256 이하에서 가장 큰 수를 경계값 L로 두고 H가 L 이상이면 메시지 뒤에 |1, |2…를 붙여 다시 계산합니다.
L = 115792089237316195423570985008687907853269984665640564039457584007913120000000
H가 L 이상일 확률은 약 8.3×10−71입니다. 실제로 다시 뽑는 일은 사실상 없지만 검산 코드에는 이 반복이 들어 있어야 원본과 같은 계산이 됩니다. 영수증의 attempt 값이 0이 아니면 메시지 끝에 그 번호가 붙었다는 뜻입니다.
클라이언트 시드 한 글자의 차이
서버 시드와 커밋은 그대로 두고 client_seed만 바꿔 다시 계산했습니다. 대소문자 하나, 끝의 공백 하나만 달라도 HMAC 전체가 바뀌어 전혀 다른 추첨값이 나옵니다.
| client_seed | HMAC 앞 8자리 | 추첨값 | 칸 |
|---|---|---|---|
c98 | 8a329eff | 84,923 | 0번 |
c99 | 1980c8c3 | 286,167 | 2번 |
C98 | af77648c | 4,399,191 | 2번 |
c98␠ (끝 공백) | 73a66e65 | 7,553,381 | 2번 |
baccaratnumbers | ddb636c4 | 904,149 | 2번 |
추첨값이 100,000 미만이면 0번, 100,000~149,999면 1번, 150,000 이상이면 2번 칸입니다.
검산이 안 맞을 때 먼저 볼 곳
- 시드 글자를 해시했다. 64자리 16진수 글자를 그대로 SHA-256에 넣으면
1cd48b66…이 나옵니다. 32바이트로 풀어서 넣어야0010d2c5…가 나옵니다. - 보상표 JSON을 다시 만들었다. 줄바꿈, 들여쓰기, 키 순서가 하나만 달라도 표 해시가 달라집니다. 받은 원문을 복사해 그대로 넣으세요.
- NFC 정규화를 빠뜨렸다. é는 합친 형태로 2바이트, 나눈 형태로 3바이트입니다. 다른 곳에서 복사해 온 글자에는 나눈 형태가 섞여 있을 수 있습니다.
- 끝 공백을 지웠다. 공백은 규칙상 그대로 남깁니다. 위 표처럼 결과가 달라집니다.
- 지금 시드로 지난 결과를 확인했다. 계정의 현재 시드나 설정은 과거 추첨에 쓰인 값과 다를 수 있습니다. 그 추첨의 영수증에 저장된 값을 쓰세요.
- 품절 칸을 빼고 더했다. 품절 칸도 가중치를 그대로 갖고 누적에 들어갑니다. 가중치가 0인 칸만 뽑히지 않습니다.
파이썬 검산 코드
Python 3 표준 라이브러리만 씁니다. proof 안의 값을 영수증의 calculation_proof 값으로 바꾸면 실제 추첨도 같은 방식으로 다시 계산할 수 있습니다. 이미 공개된 시드만 넣으세요.
import hashlib, hmac, json, unicodedata
proof = {
"brand_env": "test",
"commit_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"client_seed": "c98",
"server_seed": "62".zfill(64),
"canonical_document": '{"alg":"1","feature":"WHEEL","slots":[{"position":0,"weight":100000,"is_default":false},{"position":1,"weight":50000,"is_default":false},{"position":2,"weight":9850000,"is_default":true}]}',
}
seed = bytes.fromhex(proof["server_seed"]) # 64자리 글자가 아니라 32바이트
print("seed hash :", hashlib.sha256(seed).hexdigest())
print("table hash:", hashlib.sha256(proof["canonical_document"].encode()).hexdigest())
client = unicodedata.normalize("NFC", proof["client_seed"])
base = "|".join(["1", proof["brand_env"], "WHEEL", client, proof["commit_id"]])
TOTAL = 10_000_000
L = (2**256 // TOTAL) * TOTAL
attempt = 0
while True:
msg = base if attempt == 0 else f"{base}|{attempt}"
digest = hmac.new(seed, msg.encode(), hashlib.sha256).digest()
H = int.from_bytes(digest, "big")
if H < L:
break
attempt += 1
value = H % TOTAL
print("hmac :", digest.hex())
print("attempt :", attempt, " value:", value)
cumulative = 0
for slot in sorted(json.loads(proof["canonical_document"])["slots"], key=lambda s: s["position"]):
cumulative += slot["weight"]
if cumulative > value:
print("selected : position", slot["position"])
break
실행하면 seed hash 0010d2c5…, table hash 3e26d57e…, hmac 8a329eff…, attempt 0, value 84923, position 0이 출력됩니다. SNOW 계산기는 같은 계산을 브라우저의 Web Crypto로만 다시 합니다.
확인 시점의 SNOW 휠·검증 상태
| 항목 | 상태 |
|---|---|
| 스노우 휠 | ‘곧 열려요’, 아직 돌릴 수 없음 |
| 루트박스 | 진행 중인 보상표 없음. 외부 증명을 포함한 전체 검증은 완료되지 않았다고 표시 |
| 래플 | 이 회차에 검증 가능한 증명이 아직 연결되지 않음 |
| 외부 검증 도구 | 아직 연결되지 않음 |
| 영수증 불러오기 | PF 코드나 영수증 번호로 조회. 샘플 휠 코드는 조회되지 않음 |
| 슬롯·라이브 카지노 | 게임사 별도 시스템. 게임사 인증서는 추후 추가 예정으로 표시 |
검증 자료는 스노우 휠 결과 화면, ‘내 주문 내역’의 ‘검증’, 보너스 마켓 › ‘내 휠 내역’의 ‘공정성 확인’에서 열립니다. 휠이 열리기 전이라 이 시트는 공개 시험 벡터로만 계산했습니다.