입증된 공정성(Provably Fair) 갬블링으로 온라인 카지노가 등장한 이래 줄곧 제기된 문제를 해결했습니다. 입증된 공정성(Provably Fair) 갬블링으로 온라인 카지노가 등장한 이래 줄곧 제기된 문제를 해결했습니다. 바로 “게임 결과가 사후에 조작되지 않았다는 것을 플레이어가 어떻게 신뢰할 수 있는가?” 라는 문제입니다. 그 해답은, 서로를 신뢰하지 않는 두 당사자가 어떤 사실에 합의해야 할 때 암호학에서 사용하는 커밋-리빌(commit-reveal) 방식입니다.
이 글에서는 Cloudbet의 Mines 게임이 사용하는 전체 과정, 시드 생성부터 지뢰 배치까지 자세히 설명합니다. 그리고 조금이라도 코딩 지식이 있는 플레이어라면 누구나 자신이 플레이한 모든 결과를 직접 입증할 수 있는 방법에 대해서도 다룹니다.
Cloudbet Originals의 기반이 되는 입증된 공정성(provably fair) 메커니즘에 대해 기술적으로 상세히 분석하는 연재 시리즈의 첫 번째 아티클입니다. Mines를 포함한 모든 게임 라운드를 직접 검증하고 싶다면, Cloudbet의 입증된 공정한 계산기를 이용해 보세요.
Contents
플레이 전 결과 확정(커밋, Commit)
매 라운드가 시작되기 전, 서버는 32바이트 길이의 무작위 서버 시드(server seed)를 생성합니다. 그리고 즉시 그 시드의 SHA3-256 해시(hash)를 공개하며, 이를 확정, 즉 커밋(commitment)이라고 합니다. 이 해시는 단방향 지문과 같습니다. 즉, 시드가 무엇인지 드러내지 않으면서도 그 시점에 시드가 존재했음을 증명합니다.
게임이 종료되면 Cloudbet은 원래의 서버 시드를 공개합니다. 그러면 누구나 해시를 계산하여, 이것이 게임 시작 전 커밋과 일치하는지 확인할 수 있습니다. 이는 시드, 즉, 지뢰 배치가 첫 번째 타일을 고르기 전에 이미 정해져 있었음을 증명합니다. 카지노가 플레이 도중이나 종료 후 더 유리한 결과로 서버 시드를 바꿀 수 있는 여지는 없습니다.
플레이어의 엔트로피 반영
서버 시드만으로는 플레이어는 카지노가 만들어낸 무작위 값만 믿어야 하는 상황입니다. 이를 막기 위해 Cloudbet은 두 가지 입력값을 추가했습니다. 바로 클라이언트 시드(플레이어가 설정)와 논스(각 베팅 시에 증가하는 숫자)입니다.
이 세 가지를 모두 연결해 SHA3-256 해시로 라운드 서명을 생성합니다.
roundSignature = SHA3-256(`${serverSeed}:${clientSeed}:${nonce}`)
플레이어의 입력값인 클라이언트 시드는 서버 시드가 생성될 당시에는 알 수 없기 때문에, 카지노 측이 커밋 시점에 결과를 예측할 수 없도록 합니다. 논스는 베팅 사이에 두 시드가 모두 변경되지 않더라도, 각 라운드마다 고유한 서명이 생성되도록 합니다.
SHAKE256을 이용한 난수 생성
라운드 서명은 SHA-3 계열의 확장 가능 출력 함수(XOF)인 SHAKE256에 입력됩니다. 일반적인 해시 함수와 달리 XOF는 고정된 입력값으로부터 원하는 길이만큼의 의사난수 바이트 스트림을 생성할 수 있습니다. 여러 개의 지뢰를 배치하려면 서로 독립적인 여러 개의 난수가 필요하기 때문에 유용하게 쓰입니다.
구현 방식은 난수 스트림에서 한 번에 4바이트씩 가져와 사용합니다. 초기 출력이 모두 소진되면 상태에 "next"를 추가한 뒤 다시 출력을 생성합니다. 실제로는 25칸 그리드와 일반적인 지뢰 개수 기준으로는 처음 생성되는 256비트 출력으로도 충분합니다.
편향 없이 지뢰 배치: 기각 샘플링
랜덤 32비트 정수를 단순히 나머지 연산으로 그리드 위치에 대응시키면, 실제로는 약간의 편향이 발생합니다. 그 이유는 2^32가 남아 있는 위치의 개수(P)로 정확히 나눠지지 않는 경우가 대부분이기 때문입니다. 따라서 일부 위치는 다른 위치보다 아주 미세하게 더 높은 확률로 선택될 수 있습니다.
Cloudbet은 기각 샘플링을 통해 이러한 문제를 해결했습니다.
const maxAcceptable = Math.floor(0x100000000 / P) * P;
let rand;
do { rand = rng.next().value; } while (rand >= maxAcceptable);
const index = rand % P;
32비트 범위 안에서 P의 가장 큰 배수보다 큰 값이 나오면, 그 값은 버리고, 대신 SHAKE256 스트림에서 다음 값을 가져옵니다. 최종적으로 정확히 P개의 모든 위치가 동일한 확률을 갖는 균등분포를 만족하게 됩니다.
각 지뢰가 배치될 때마다 해당 위치는 풀에서 제거되고 P도 1씩 감소합니다. 이는 25칸 평면 그리드 배열에 대한 부분 Fisher-Yates 셔플과 동일합니다.
라운드 입증하기
경기 후 입증에는 네 가지 값이 필요하며, 이 값들은 모두 해당 라운드가 끝난 후 Cloudbet에서 공개됩니다.
서버 시드클라이언트 시드논스- 공개된
지뢰의 위치
입증 단계는 다음과 같습니다.
서버 시드를 SHA3-256으로 해싱하고 게임 시작 전 커밋과 일치하는지 확인합니다.- 다음 식으로 라운드 서명을 도출합니다.
roundSignature = SHA3-256(serverSeed:clientSeed:nonce). 라운드 서명으로 SHAKE256 스트림을 초기화한 뒤, 기각 샘플링 루프를 통해지뢰 위치를 독립적으로 재현합니다.
일치한다면, 라운드가 시작된 후 그리드가 변경되지 않았다는 것을 의미합니다.
구현 예시 (Node.js)
npm install js-sha3
const crypto = require('crypto');
const { sha3_256, shake256 } = require('js-sha3');
function generateServerSeed() {
const seed = crypto.randomBytes(32).toString('hex');
return { serverSeed: seed, commitment: sha3_256(seed) };
}
function createRoundSignature(serverSeed, clientSeed, nonce) {
return sha3_256(`${serverSeed}:${clientSeed}:${nonce}`);
}
function* createShake256Stream(signature) {
const byteStream = shake256.create(32);
byteStream.update(signature);
while (true) {
const buf = Buffer.from(byteStream.digest({ buffer: true }));
for (let i = 0; i < buf.length; i += 4) {
if (i + 4 <= buf.length) yield buf.readUInt32BE(i);
}
byteStream.update('next');
}
}
function pickUniquePositions(count, totalTiles, rng) {
const available = Array.from({ length: totalTiles }, (_, i) => i);
const result = [];
for (let i = 0; i < count; i++) {
const P = available.length;
const maxAcceptable = Math.floor(0x100000000 / P) * P;
let rand;
do { rand = rng.next().value; } while (rand >= maxAcceptable);
const index = rand % P;
result.push(available[index]);
available.splice(index, 1);
}
return result;
}
const { serverSeed, commitment } = generateServerSeed();
const clientSeed = 'player-provided-seed';
const nonce = 0;
const sig = createRoundSignature(serverSeed, clientSeed, nonce);
const rng = createShake256Stream(sig);
const mines = pickUniquePositions(5, 25, rng);
console.log('Commitment:', commitment);
console.log('Server seed (post-game):', serverSeed);
console.log('Mine positions:', mines);
주목할 만한 몇 가지 사항
왜 SHA2 대신 SHA3인가요? 간단한 커밋이라면 둘 중 어느 쪽이든 괜찮습니다. 하지만 SHA3의 스펀지 구조는 길이 확장 공격을 방지하며, 커밋 연산과 SHAKE256의 기반으로 파이프라인 전체에 이를 사용함으로써 암호화 표면의 일관성을 유지할 수 있습니다.
시드가 지정된 PRNG 대신 SHAKE256을 사용하는 이유는 무엇인가요? 결정성과 감사 가능성 때문입니다. SHAKE256은 명확하게 정의된 사양을 가진 표준화된 기본 요소이므로, 어떤 언어로 구현하든 동일한 입력에서 동일한 출력을 생성합니다. 커스텀 PRNG는 구현에 따라 달라지기 때문에, 제3자에 의한 검증이 더 어렵습니다.
플레이어가 유리한 클라이언트 시드를 선택하여 시스템을 악용할 수 있나요? 이론적으로는 가능합니다. 플레이어는 다양한 클라이언트 시드를 시험해 보면서 자신에게 유리한 그리드가 생성되는 시드를 찾을 수 있습니다. 이는 설계상 허용되는 것으로 간주됩니다. 플레이어가 시드를 선택할 수 있는 권리는 취약점이 아니라 기능입니다. 카지노의 리스크 노출 구조는 대칭적이며, 무엇보다도 플레이어가 원하는 클라이언트 시드를 선택할 수 있게 하는 동일한 시스템이 카지노가 서버 시드를 조작하지 않았다는 사실까지 검증할 수 있다는 점이 중요합니다.
Mines 게임은 입증된 공정성 설계를 살펴볼 수 있는 훌륭한 예시입니다. 단일 커밋-리빌 사이클, 간단한 바이트 스트림, 그리고 잘 알려진 샘플링 기법을 갖추고 있는 등 RNG 파이프라인이 비교적 독립적으로 구성되어 있기 때문입니다. 시리즈가 진행됨에 따라, 다른 Cloudbet Originals 작품들이 이러한 기반을 어떻게 적용하고 확장하는지 살펴보게 될 것입니다. 때로는 추가적인 엔트로피 소스, 다른 출력 매핑, 또는 다단계 검증 방식을 도입하기도 하겠지만, 플레이어의 행동이 확정된 후에는 어떤 결과도 새로 결정될 수 없다는 핵심 보장은 그대로 유지됩니다.


