이 결과, 정말 조작되지 않았을까?
온라인에서 랜덤 결과를 제공하는 서비스를 만들거나 사용할 때, 한 번쯤은 이런 의문을 가질 수 있습니다. 특히 결과에 돈이 걸려 있다면, 운영자가 결과를 조작해도 사용자는 알 방법이 없습니다.
Kristóf Poduszló의 연구 「Introduction to Provably Fair Gaming Algorithms」에 따르면, 오늘날 대부분의 온라인 게임 산업은 블랙박스 알고리즘을 사용하여 사용자가 제3자 서비스의 난수 생성을 맹목적으로 신뢰해야 합니다. 이에 대한 해결책으로 등장한 것이 Provably Fair(증명 가능한 공정성)입니다.
Provably Fair는 암호학적 기법을 사용해 운영자와 사용자가 서로를 믿을 필요 없이, 결과가 공정하게 만들어졌음을 수학적으로 검증할 수 있게 합니다. 모든 참여자가 게임 결과의 난수 생성에 동등한 영향력을 가지며, 이를 검증 가능한 방식으로 보장하는 것이 핵심입니다.
이 글에서는 비트코인 도박 커뮤니티에서 널리 알려진 Provably Fair 구현 방식을 살펴보고, 파이썬으로 구현한 provablyfair 라이브러리를 소개합니다. 개발자로서 이 알고리즘이 어떻게 동작하는지 이해하면, 왜 '공정한 도박'도 결국 위험한지 수학적으로 확인할 수 있습니다.
온라인 도박에서 결과는 서버에서 만들어집니다.
# 서버 내부에서 일어나는 일 (우리는 볼 수 없음)
import random
result = random.randint(1, 100)사용자 입장에서 이것은 블랙박스입니다. 운영자가 if 사용자가_따면: 다시_굴리기를 넣어도 알 방법이 없습니다.
Provably Fair의 핵심 아이디어는 간단합니다.
이렇게 하면 운영자가 결과를 사후에 조작할 수 없습니다.
가장 널리 쓰이는 Provably Fair 구현은 세 가지 변수로 결과를 계산합니다.
| 변수 | 제공자 | 역할 |
|---|---|---|
| Server Seed | 운영자 | 결과의 기반이 되는 비밀 값 |
| Client Seed | 사용자 | 운영자가 결과를 예측하지 못하도록 하는 값 |
| Nonce | 베팅 시스템 | 매 베팅마다 1씩 증가하는 카운터 |
게임이 시작되기 전, 운영자는 서버 시드의 SHA-256 해시를 사용자에게 미리 공개합니다.
서버 시드 (비공개):
293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04
서버 시드 해시 (사전 공개):
5ac59780d512265230d5efb3cc238886dc1b457a80b54fbf1f920b99c6505801
이것이 중요한 이유는 커밋먼트 스킴(commitment scheme)이 되기 때문입니다.
커밋먼트 스킴은 암호학에서 값을 미리 약속(commit)하고 나중에 공개(reveal)하는 프로토콜입니다. Poduszló의 논문에서는 이를 비대칭 암호화에 빗어 설명합니다. hostSeed는 게임 종료 전까지 비밀로 유지되는 개인키와 같은 역할을 하고, publicSeed는 해시가 공개된 후에야 생성되거나 참여자에 의해 제공됩니다.
핵심은 두 가지 성질입니다.
SHA-256 같은 암호학적 해시 함수는 이 두 성질을 모두 만족합니다. 해시를 먼저 공개한 이상 운영자는 게임 도중 서버 시드를 바꿀 수 없습니다. 나중에 원본 시드를 공개했을 때 해시가 일치하는지 확인하면 그만입니다. 이는 게임 중 hostSeed가 변조되지 않았음을 보장하는 무결성 증명이 됩니다.
그렇다고 사용자가 시드를 미리 알면 안 됩니다. 시드를 알면 결과를 전부 미리 계산할 수 있기 때문입니다. 해시만 먼저 공개하고 원본은 게임 종료 후 공개하는 '나중에 열어보는 봉인 봉투' 방식입니다.
실제로 위 해시가 맞는지 직접 확인해 볼 수 있습니다.
echo -n "293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04" | sha256sum
# 5ac59780d512265230d5efb3cc238886dc1b457a80b54fbf1f920b99c6505801실제 결과는 다음과 같이 계산합니다.
HMAC은 키가 있는 해시 함수입니다. 서버 시드를 키로, 클라이언트시드-nonce를 메시지로 사용합니다.
예를 들어 위에서 공개한 시드와 user_seed_123이라는 클라이언트 시드, nonce 0으로 계산하면 다음과 같은 해시가 나옵니다.
python3 -c "
import hmac
from hashlib import sha512
print(hmac.new(
b'293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04',
b'user_seed_123-0',
sha512
).hexdigest())
"
# cd1d40d74cb1e1fb548ee7adb7e14c7cc71ead5328e425fbb2f2324e064ea8107abf327d21131019575f244f87f54a02bd7a22110cef9e58b272acc7afffc88f이 해시의 앞 5글자 cd1d4를 16진수에서 10진수로 변환하면 가 됩니다. 이 값이 백만()을 넘지 않으므로 이 값을 사용합니다.
int('cd1d4', 16) # 840148최종적으로 라는 결과가 나옵니다. 이 숫자가 주사위 결과입니다.
게임이 끝나면 운영자는 원본 서버 시드를 공개합니다. 사용자는 다음을 검증합니다.
두 가지 모두 맞다면 결과는 조작되지 않은 것입니다. 운영자는 자신도 결과를 미리 알 수 없고(클라이언트 시드는 사용자가 정하므로), 사후에 바꿀 수도 없습니다(시드를 바꾸면 해시가 안 맞으므로).
다만 주의할 점이 있습니다. 검증이 가능하다는 것은 '공정하다'는 뜻이지, '유리하다'는 뜻이 아닙니다. 게임 규칙 자체가 어떻게 설계되었는지는 별개의 문제입니다.
이 글에서 다룬 방식은 단일 플레이어가 운영자와 대결하는 1:1 구조에 집중합니다. 하지만 여러 명이 동시에 베팅하는 게임이나, 플레이어 간에 대결하는 카드 게임에서는 더 복잡한 문제가 생깁니다.
이러한 다중 플레이어 환경의 세부 구현이나 Fisher-Yates 셔플의 암호학적 적용 방법은 Kristóf Poduszló의 원문 논문을 참조하세요.
이 알고리즘을 파이썬 라이브러리로 만들었습니다. 설치는 간단합니다.
pip install provablyfairfrom provablyfair import ProvablyFair
f = ProvablyFair("293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04")
r = f.roll("user_seed_123")
print(r.roll % (10000) / 100) # 1.48
print(r.server_seed_hash) # 5ac59780d... (사전 공개용 해시)ProvablyFair 생성자에 서버 시드를 전달하거나, 생략하면 secrets.token_hex(32)로 안전한 랜덤 시드를 자동 생성합니다. 객체를 만들 때 서버 시드 해시도 함께 계산되므로, 게임 시작 전에 사용자에게 공개할 수 있습니다.
from provablyfair import ProvablyFair, verify_roll, RolledData
f = ProvablyFair("293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04")
r = f.roll("user_seed_123")
# 나중에 다른 곳에서 검증
verified_data = RolledData(
roll=840148,
nonce=0,
server_seed_hash='5ac59780d512265230d5efb3cc238886dc1b457a80b54fbf1f920b99c6505801',
client_seed='user_seed_123'
)
print(verify_roll(f.server_seed, verified_data)) # Trueverify_roll는 서버 시드와 저장된 결과 데이터를 받아 처음부터 다시 계산한 뒤 비교합니다. 해시 일치 여부와 결과 재현 여부를 모두 확인하는 것입니다.
라이브러리의 핵심 로직은 약 40줄입니다. 수학적 복잡성보다는 관례에 대한 정확한 구현이 중요합니다.
from dataclasses import asdict, dataclass
from hashlib import sha256, sha512
from hmac import new
from secrets import token_hex
class ProvablyFair:
def __init__(self, server_seed: str = None) -> None:
self.server_seed = server_seed or self.generate_server_seed()
self.server_seed_hash = self.hash_server_seed(self.server_seed)
def generate_server_seed(self) -> str:
return token_hex(32)
def hash_server_seed(self, server_seed: str):
return sha256(server_seed.encode()).hexdigest()
def roll(self, client_seed: str, nonce: int = 0) -> RolledData:
hmac_hash = new(
self.server_seed.encode(),
f"{client_seed}-{nonce}".encode(),
sha512,
).hexdigest()
count = 0
while True:
roll_number_str = hmac_hash[count : count + 5]
roll_number = int(roll_number_str, 16)
if roll_number > 999999:
count += 5
else:
break
return RolledData(roll_number, nonce, self.server_seed_hash, client_seed)몇 가지 설계 포인트가 있습니다.
RolledData는 @dataclass로 정의되어 결과와 검증에 필요한 모든 정보를 담습니다.이제 결과를 만들고 검증하는 방법은 충분히 다뤘습니다. 이 알고리즘으로 생성된 숫자는 암호학적으로 '공정'합니다. 운영자도 결과를 미리 알 수 없고, 베팅 후에 바꿀 수도 없습니다.
그런데 여기서 한 가지 의문이 남습니다. 왜 이렇게까지 검증 가능성을 강조할까요? 그리고 공정한 결과가 나온다고 해서, 사용자에게 정말 유리한 게임일까요?
'공정함'을 검증할 수 있다는 사실에 집중하다 보면, 정작 중요한 것을 놓치기 쉽습니다. 결과가 조작되지 않는다는 것과 결과가 유리하다는 것은 완전히 다른 이야기입니다.
전형적인 비트코인 다이스 게임의 규칙을 살보면, 공정함 뒤에 숨겨진 수학적 장치가 보입니다. 이것이 하우스 엣지(House Edge)입니다.
전형적인 다이스 게임의 규칙입니다.
여기서 함정이 있습니다. 승리 확률은 49.5%지만, 배당률은 2배가 아니라 1.98배입니다. 이 0.02의 차이가 의미하는 바를 계산해 보겠습니다.
이 0.02의 차이가 실제로 얼마나 큰 의미인지 수학적으로 계산해 보겠습니다.
베팅 1회당 평균 1.99% 손실입니다. 얼마 안 되는 것처럼 보이지만, 이것이 하우스 엣지의 본질입니다. 운영자는 매 베팅에서 약 2%씩 가져가는 구조이고, 이것은 베팅 횟수가 늘어날수록 누적됩니다.
def expected_value(win_prob: float, payout_multiplier: float) -> float:
"""1회 베팅의 기대값 (베팅금 1 단위 기준)"""
if win_prob <= 0 or win_prob >= 1:
raise ValueError("승리 확률은 0과 1 사이여야 함")
win_return = payout_multiplier - 1 # 순이익 (베팅금 제외)
lose_return = -1 # 베팅금 손실
ev = (win_prob * win_return) + ((1 - win_prob) * lose_return)
return ev
# 다이스 게임 예시
print(expected_value(0.495, 1.98)) # -0.0199...이 구조에서 사용자는 결코 이길 수 없습니다. 베팅을 반복할수록 큰 수의 법칙이 작동합니다.
import random
from provablyfair import ProvablyFair
def simulate_gambling(rolls: int = 10000, house_edge: float = 0.0199):
"""도박 시뮬레이션: 공정한 랜덤에서도 하우스 엣지만으로 손실 발생"""
pf = ProvablyFair()
balance = 1000.0 # 초기 자본
bet_amount = 10.0
for i in range(rolls):
r = pf.roll(f"user_seed_{i}")
roll_result = (r.roll % 10000) / 100 # 0.00 ~ 99.99
# 49.5 미만에 베팅 (승리 확률 49.5%)
if roll_result < 49.5:
balance += bet_amount * 0.98 # 1.98배 배당
else:
balance -= bet_amount
if balance <= 0:
break
return balance
# 시뮬레이션 실행
final_balance = simulate_gambling()
print(f"10,000회 베팅 후 잔액: {final_balance:.2f}") # 초기 1000에서 손실실제로 실행보면 '공정한' 랜덤 결과에서조차 잔액은 꾸준히 감소합니다. 하우스 엣지가 작동하기 때문입니다. 이것이 도박의 수학적 본질입니다.
'공정하게 지는 것'과 '조작당해서 지는 것'의 차이는 사용자 경험에는 큰 차이가 없습니다. 둘 다 결국 돈을 잃습니다.
그렇다면 왜 운영자들은 Provably Fair를 내세울까요? 답은 간단합니다. 조작의 의심을 없애 신뢰를 얻으면, 더 많은 사용자가 더 오래 베팅하기 때문입니다. 하우스 엣지가 있다면 사용자가 많을수록, 오래 플레이할수록 운영자의 수익은 보장됩니다.
'공정한 랜덤'은 도박의 본질을 바꾸지 않습니다. 기술적으로는 흥미로운 개념이지만, 도박의 맥락에서는 사용자의 불신을 해소해 더 많은 참여를 유도하는 수단이 됩니다.
Provably Fair는 암호학을 사용해 신뢰를 프로그래밍하는 흥미로운 알고리즘입니다. 블록체인, 추첨 시스템, 온라인 투표 등 다양한 곳에 응용될 수 있습니다. provablyfair 라이브러리도 그러한 기술적 학습의 일환으로 만들어졌습니다.
다만 도박 맥락에서는 '검증 가능한 공정성'이 결과 생성 과정의 투명성만 보장할 뿐, 게임의 수학적 구조가 사용자에게 불리하다는 사실은 바꾸지 않습니다. 하우스 엣지가 있는 한, 사용자가 장기적으로 이길 확률은 0%입니다.
기술을 이해한다는 것은 그 기술이 어떤 목적으로 사용되는지까지 파악하는 것을 포함합니다. Provably Fair의 원리를 아는 것은 도박의 메커니즘을 이해하는 것이고, 이는 도박으로부터 자신을 지키는 지식이 될 수 있습니다.
provablyfair는 GitHub에서 MIT 라이선스로 공개되어 있습니다. 이 라이브러리는 교육 목적이며, 어떤 형태의 도박도 권장하지 않습니다.